Skip to content

feat(products): add designer customizable products#105

Merged
henri318 merged 16 commits into
mainfrom
feat/designer-customizable-products
Jul 3, 2026
Merged

feat(products): add designer customizable products#105
henri318 merged 16 commits into
mainfrom
feat/designer-customizable-products

Conversation

@henri318

@henri318 henri318 commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Issue\nN/A — this repository does not require a GitHub issue for PRs.\n\n## Summary\n- Add designer-owned product create/edit flows with seller ownership checks.\n- Seed customizable products with Docker-served product assets and PrismaPg warning regression coverage.\n- Persist product translations/customization config and emit product created/updated outbox events.\n\n## Test Plan\n- [x] npm run typecheck\n- [x] npm run lint\n- [x] vitest focused product/seller/prisma suites\n- [x] pre-push hook passed on push\n\n## Notes\n- Stacked-to-main PR; more customization work will continue in follow-up slices.\n- Includes merge from origin/main and one feature commit.

Summary by CodeRabbit

  • New Features

    • Se ha añadido una experiencia de personalización de productos con vista previa, carga de imágenes y opciones de texto, color y tamaño.
    • La cesta y el checkout ahora muestran mejor las personalizaciones, incluyendo miniaturas y enlaces para editarlas.
    • El área de vendedor incorpora creación y edición de productos con fotos, configuración de personalización y búsqueda mejorada.
  • Bug Fixes

    • Se mejoró la gestión de carritos y pedidos para conservar correctamente los datos de personalización.
    • Se reforzó la carga y visualización de imágenes locales y de vista previa.

Developer and others added 7 commits June 29, 2026 15:51
…customer creation API

- Add ProductCustomizationConfig VO with Zod parsing and default fallback
- Add Prisma migration for Product.customizationConfig JSONB column and UploadType.customization enum
- Create customer-facing POST /api/customizations/customer route (CUSTOMER role)
- Add CreateCustomerCustomization use case with capability-aware validation
- Implement POST /api/uploads/guest/presigned-url for guest photo uploads
- Extend MigrateGuestCart to resolve guest upload IDs and create Customization entities
- Add CustomizationCreatePort to cart domain for decoupled customization creation
- Add unit tests covering capability parsing, use case, routes, and guest cart migration

Verification: 32/32 targeted tests pass, 350/350 regression tests pass, typecheck clean.
feat(customizations): add product customization capability model and customer creation API
feat(customizations): add PDP customization preview experience
@henri318 henri318 added the type:feature New feature label Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@henri318, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a76dd4a-9bf0-41af-bfc1-8a474d9e962e

📥 Commits

Reviewing files that changed from the base of the PR and between baff07d and ed2e272.

📒 Files selected for processing (7)
  • app/[locale]/products/[id]/page.module.css
  • app/[locale]/products/[id]/page.tsx
  • app/api/uploads/local/[...storageKey]/route.ts
  • prisma/seed.ts
  • tests/e2e/auth/change-password.spec.ts
  • tests/e2e/auth/profile.spec.ts
  • tests/e2e/products/products.spec.ts
📝 Walkthrough

Walkthrough

Changes

Personalización de producto end-to-end

Layer / File(s) Summary
Modelo de dominio y puertos
modules/products/domain/value-objects/product-customization-config.ts, modules/products/domain/entities/product.ts, modules/products/domain/product-capability-port.ts, modules/cart/domain/customization-create-port.ts, modules/customizations/domain/...
ProductCustomizationConfig, DesignPosition, CustomizationEntity y sus puertos (ProductCapabilityPort, CustomizationCreatePort) definen modos, capacidades y offsets de personalización.
Esquema BD, migración y repositorios
prisma/schema.prisma, prisma/migrations/..., modules/products/infrastructure/mapper.ts, modules/products/infrastructure/prisma-product-repository.ts, modules/customizations/infrastructure/prisma-customization-repository.ts, prisma/seed.ts
Añade columnas customizationConfig/designPosition (JSONB), mapea JSON↔dominio y cambia lecturas/escrituras a ejecución secuencial en varios repositorios.
Casos de uso y API de producto
modules/products/application/{create,update}-product-use-case.ts, app/api/products/[id]/route.ts, modules/products/presentation/...
CreateProductUseCase/UpdateProductUseCase con validaciones y el endpoint PATCH /api/products/[id] protegido por rol.
Personalización de cliente
modules/customizations/application/*, app/api/customizations/*
CreateCustomerCustomization valida capacidades del producto y expone/ajusta endpoints /api/customizations/* con designPosition.
Subida de invitados y almacenamiento local
app/api/uploads/guest/presigned-url/route.ts, app/api/uploads/local/[...storageKey]/route.ts, modules/uploads/infrastructure/local-storage-adapter.ts
Endpoint de subida con rate limiting por dispositivo y almacenamiento local basado en filesystem.
UI de personalización en PDP
app/[locale]/products/[id]/*
CustomizationDraftContext, CustomizationForm, CustomizationPreview, MockupCanvasControl, PhotoUploadField y CustomizationExperience conforman el flujo de personalización visual.
Integración en carrito/checkout/pedidos
modules/cart/**, app/api/cart/**, app/[locale]/checkout/page.tsx, modules/orders/**
Propaga designPosition a través de la migración del carrito invitado, AddToCartButton, vistas de carrito y snapshots de personalización en pedidos.
Panel del vendedor
app/[locale]/seller/products/**, shared/{presentation,ui}/components/file-upload-dropzone*
Páginas de creación/edición de producto, ProductForm, ProductPhotoGallery y el editor de configuración de personalización.
Infraestructura de desarrollo
docker-compose*.yml, package.json, next.config.ts, shared/i18n/locales/*, AGENTS.md
Servicio nginx de assets, scripts dev, patrones de imágenes remotas y traducciones nuevas.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Comprador
  participant AddToCartButton
  participant API_Customizations as API /api/customizations/customer
  participant CreateCustomerCustomization
  participant MigrateGuestCart
  participant GuestCartContext

  Comprador->>AddToCartButton: Añadir producto con diseño
  AddToCartButton->>GuestCartContext: addItem(customización, designPosition)
  Comprador->>AddToCartButton: Iniciar sesión / checkout
  AddToCartButton->>API_Customizations: POST {productId, text, imageUrl, designPosition}
  API_Customizations->>CreateCustomerCustomization: execute(dto, userId)
  CreateCustomerCustomization-->>API_Customizations: CustomizationEntity
  API_Customizations->>MigrateGuestCart: execute(guestItems)
  MigrateGuestCart-->>AddToCartButton: carrito migrado con personalización
Loading

Possibly related PRs

  • henri318/728-store#1: Amplía la misma página de checkout para renderizar previsualización y propagar designPosition en las customizations.
  • henri318/728-store#74: Modifica la misma lógica de migración/normalización de personalizaciones del carrito invitado (migrate-guest-cart.ts).
  • henri318/728-store#98: Modifica el mismo flujo backend de personalización de cliente y migración de carrito invitado.

Suggested labels: type:feature

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed El título resume correctamente la incorporación de productos personalizables por diseñador.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/designer-customizable-products

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/[locale]/cart/cart-view.tsx (1)

267-283: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

La sección de personalización renderiza un <span> vacío cuando solo hay imageUrl.

Se añadió item.customization.imageUrl a la condición de visibilidad (Líneas 269-270), pero el contenido del <span> solo concatena size, color y text. Si únicamente está presente imageUrl, la condición es verdadera pero filter(Boolean).join(' · ') produce una cadena vacía, mostrando un <span class={styles.customization}> vacío (con posible padding/margen). La imagen ya se renderiza por separado en las Líneas 303-311, por lo que el imageUrl no debería formar parte de esta condición.

🐛 Corrección propuesta
                 {(item.customization.text ||
                   item.customization.color ||
-                  item.customization.size ||
-                  item.customization.imageUrl) && (
+                  item.customization.size) && (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/cart/cart-view.tsx around lines 267 - 283, The customization
block in cart-view.tsx can render an empty span when only imageUrl is present
because the visibility check includes item.customization.imageUrl but the
content only formats size, color, and text. Update the conditional around the
customization <span> in CartView so it only depends on fields actually rendered
there, and keep imageUrl handling separate since it is already rendered
elsewhere. Use the existing item.customization checks and
labels.customizationSize/customizationColor/customizationText as the reference
points when adjusting the condition.
modules/cart/application/migrate-guest-cart.ts (1)

119-202: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Evita crear personalizaciones antes de aplicar keep-server.
En keep-server con un carrito existente, resolveGuestCustomizationIds puede persistir nuevas personalizaciones y luego availableGuest se descarta sin asociarlas al carrito, dejando filas huérfanas. Mueve esa creación a la rama que realmente aplica los ítems, o sáltala en ese caso.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/application/migrate-guest-cart.ts` around lines 119 - 202, Avoid
creating guest customizations before the keep-server branch is resolved. In
migrate-guest-cart, resolveGuestCustomizationIds is currently called while
building availableGuest, so it can persist new customizations even when
dto.strategy is keep-server and the existing cart is left unchanged. Move the
customization resolution into the branches that actually build or merge items
(or guard it so keep-server with a non-new cart skips creation), using
migrate-guest-cart, resolveGuestCustomizationIds, availableGuest, and the
dto.strategy checks as the key locations.
🧹 Nitpick comments (12)
app/[locale]/seller/products/new/page.tsx (1)

24-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicación del mapeo de labels entre las páginas de crear y editar.

Este bloque de labels es prácticamente idéntico al de app/[locale]/seller/products/[id]/edit/page.tsx (salvo title/save). Considera extraer un helper compartido, p. ej. buildProductFormLabels(dict), para evitar que ambas listas se desincronicen al añadir nuevas etiquetas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/new/page.tsx around lines 24 - 42, The labels
mapping in the product create page is duplicated with the edit page and can
drift over time. Extract the shared label construction into a helper such as
buildProductFormLabels(dict) and reuse it from both the create and edit page
components, while keeping only the page-specific title/save overrides in the
respective page files.
tests/unit/prisma/prisma-seed-config.test.ts (1)

28-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Aserciones frágiles basadas en coincidencia de texto del código fuente.

Comprobar not.toContain('Promise.all(') y la presencia literal de for (const role of [ / for (const p of productsData) acopla el test al formato y a los nombres exactos del código fuente. Un refactor inocuo (renombrar variables, reordenar bucles, o usar otro patrón secuencial igualmente válido) romperá el test sin que exista una regresión real; y a la inversa, un Promise.all introducido en una ruta no secuencial no se detectaría correctamente. Considera validar el comportamiento real (p. ej. mockear el cliente y verificar que las escrituras ocurren en serie) en lugar de inspeccionar el texto.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/prisma/prisma-seed-config.test.ts` around lines 28 - 34, The test
in prisma-seed-config.test.ts is brittle because it asserts against literal
source text instead of behavior. Update the seed test to verify sequential
writes by exercising the seed logic with a mocked Prisma/client layer and
asserting the write calls happen in order, rather than checking for Promise.all
or exact loop text. Use the seed-related symbols like seedPath and the seeding
routine under test to locate the behavior and keep the assertion resilient to
harmless refactors.
app/[locale]/seller/products/product-form.tsx (1)

273-275: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Etiqueta del botón redundante durante la carga.

submitLabel es igual a labels.save, por lo que loading ? labels.save : submitLabel muestra el mismo texto en ambos estados. Conviene usar una etiqueta tipo "Guardando…" mientras loading es true para dar feedback visual.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/product-form.tsx around lines 273 - 275, The
submit button in the product form is showing the same text in both states
because submitLabel matches labels.save; update the button rendering in
product-form so it uses a distinct loading label while loading is true and the
normal submitLabel only when idle. Use the existing button state in the form
component to switch between a “saving” message and the default submit text, so
the user gets clear feedback during submission.
prisma/seed.ts (1)

122-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

El log customizable refleja previewEnabled, no la capacidad de personalización.

previewEnabled indica si hay previsualización, no si el producto es personalizable. Un producto con mode: 'description' (como la sudadera) es personalizable pero se loguea como customizable: no. Es solo un mensaje de seed, pero induce a confusión; considera derivarlo de customizationConfig.mode !== 'description' o renombrar la etiqueta a preview.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/seed.ts` around lines 122 - 124, The product seed log in the create
block is using customizationConfig.previewEnabled to print the “customizable”
label, which misrepresents products like those with mode set to description.
Update the console.log in prisma/seed.ts near the product creation flow to
either derive the label from customizationConfig.mode instead of previewEnabled,
or rename the label to “preview” so it matches the field being reported; use the
existing getSeedProductLabel(p) and product.id context to keep the message
consistent.
modules/products/presentation/schemas/product-form-schema.ts (1)

30-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sustituir z.nativeEnum por z.enum

ProductStatus es un enum nativo de TypeScript y Zod 4 ya lo admite en z.enum(...); z.nativeEnum(...) está deprecado.

♻️ Cambio propuesto
-    status: z.nativeEnum(ProductStatus).optional(),
+    status: z.enum(ProductStatus).optional(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/presentation/schemas/product-form-schema.ts` at line 30,
Update the product form schema to stop using the deprecated Zod helper: in
product-form-schema.ts, replace the ProductStatus validation in the schema
definition with z.enum(...) instead of z.nativeEnum(...), keeping the field
optional and preserving the existing ProductStatus values. Use the schema object
in the product form schema as the place to make the change.
modules/customizations/presentation/schemas/customization-schemas.ts (1)

34-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicación exacta de createCustomizationSchema.

createCustomerCustomizationSchema (líneas 34-38) es idéntico byte a byte a createCustomizationSchema (líneas 28-32). Mientras no diverjan, conviene reutilizar el esquema existente para evitar que se desincronicen ante cambios futuros.

♻️ Refactor propuesto
-export const createCustomerCustomizationSchema = customizationFieldsSchema
-  .extend({
-    productId: z.string().min(1, 'Product ID is required'),
-  })
-  .strict();
+export const createCustomerCustomizationSchema = createCustomizationSchema;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/customizations/presentation/schemas/customization-schemas.ts` around
lines 34 - 38, `createCustomerCustomizationSchema` is an exact duplicate of
`createCustomizationSchema`; refactor it to reuse the existing schema instead of
rebuilding the same `customizationFieldsSchema.extend(...).strict()` chain.
Locate the duplicated schema definitions in `customization-schemas.ts` and make
`createCustomerCustomizationSchema` reference the shared
`createCustomizationSchema` (or extract a common base helper) so both stay in
sync with future changes.
components/cart/add-to-cart-button.tsx (1)

133-208: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validación duplicada en ambas ramas.

El bloque normalizeCustomizationDraft + customizationDraftSchema.safeParse con su manejo de error es idéntico en la rama autenticada (133-143) y en la de invitado (197-207). Se puede elevar una sola vez antes del if (isAuthenticated).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/cart/add-to-cart-button.tsx` around lines 133 - 208, The
customization validation logic is duplicated in both the authenticated and guest
branches, so move the shared normalizeCustomizationDraft plus
customizationDraftSchema.safeParse check (including the error state handling) to
a single place before the isAuthenticated conditional in add-to-cart-button.tsx.
Keep the existing success/failure behavior the same, but have both branches
reuse the already validated normalizedCustomization instead of repeating the
same block.
app/api/customizations/customer/route.ts (1)

30-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rama inalcanzable en getConfig.

assertAllowed solo invoca getConfig con dto.productId, que aquí siempre coincide con product.id. La rama productId !== product.id (líneas 32-35) nunca se ejecuta y genera una segunda lectura del repositorio que no aporta. Se puede simplificar devolviendo directamente la config del producto ya cargado.

♻️ Simplificación propuesta
     const capabilityPort: ProductCapabilityPort = {
-      async getConfig(productId: string) {
-        if (productId !== product.id) {
-          const other = await productRepository.findById(productId, 'es');
-          return other?.customizationConfig ?? null;
-        }
-
-        return (
-          product.customizationConfig ?? ProductCustomizationConfig.default()
-        );
-      },
+      async getConfig() {
+        return (
+          product.customizationConfig ?? ProductCustomizationConfig.default()
+        );
+      },
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/customizations/customer/route.ts` around lines 30 - 41, The
`getConfig` method in `capabilityPort` contains an unreachable `productId !==
product.id` branch because `assertAllowed` only calls it with the current
product’s id. Remove that branch and the extra `productRepository.findById`
lookup, and have `getConfig` return the already loaded
`product.customizationConfig` or `ProductCustomizationConfig.default()`
directly.
app/api/cart/migrate/route.ts (1)

73-120: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Lecturas de producto secuenciales y posiblemente duplicadas.

Se construyen dos mapas (capabilityProductMap en 77-82 y productMap en 117-120) recorriendo IDs con await findById uno a uno (N+1), y ambos pueden solicitar los mismos productos dos veces. Si el repositorio ofrece un findManyByIds/batch, conviene usarlo y, si los conjuntos de IDs se solapan, reutilizar las entidades ya cargadas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/cart/migrate/route.ts` around lines 73 - 120, The cart migration flow
is doing sequential N+1 product lookups twice via `productsModuleRepo.findById`
when building `capabilityProductMap` and `productMap`, and the same products may
be fetched again. Update `MigrateGuestCart` usage in this route to batch-load
product data if `productsModuleRepo` exposes a `findManyByIds`/similar method,
then populate both maps from that shared result set. If the ID sets overlap,
reuse the already loaded entities instead of querying them again.
app/[locale]/cart/cart-view.tsx (1)

284-302: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Evita invocar buildCustomizationHref tres veces por ítem.

En el render se llama a buildCustomizationHref en la condición y dos veces más para el href (Líneas 285, 292), recreando URLSearchParams en cada llamada. Calcula el resultado una sola vez antes del JSX y reutilízalo.

♻️ Refactor propuesto
+                {(() => {
+                  const editHref = labels.customizationEditFromCart
+                    ? buildCustomizationHref(locale, item.productId, item.customization)
+                    : null;
+                  return editHref ? (
+                    <a href={editHref} className={styles.editLink}>
+                      {labels.customizationEditFromCart}
+                    </a>
+                  ) : null;
+                })()}
-                {labels.customizationEditFromCart &&
-                  buildCustomizationHref(
-                    locale,
-                    item.productId,
-                    item.customization,
-                  ) && (
-                    <a
-                      href={
-                        buildCustomizationHref(
-                          locale,
-                          item.productId,
-                          item.customization,
-                        ) ?? '#'
-                      }
-                      className={styles.editLink}
-                    >
-                      {labels.customizationEditFromCart}
-                    </a>
-                  )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/cart/cart-view.tsx around lines 284 - 302, The cart item edit
link in cart-view.tsx calls buildCustomizationHref multiple times per item,
recreating URLSearchParams unnecessarily. Compute the customization URL once
before the JSX in the cart item render path, store it in a local variable, and
reuse that value for both the condition and the href in the edit link. Keep the
fix centered around buildCustomizationHref and the cart item rendering logic.
tests/unit/app/api/uploads/guest/presigned-url/route.test.ts (1)

54-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Falta cobertura del camino de rate limiting (429).

El control de tasa es la lógica diferenciadora de este endpoint, pero solo se prueban los casos 400 y 201. Conviene añadir un test que fuerce rateLimitMock a devolver { blocked: true, reason: 'ip', retryAfterSeconds: 60 } y verifique el status 429, el header Retry-After y que recordLoginAttempt no se invoca.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/app/api/uploads/guest/presigned-url/route.test.ts` around lines 54
- 83, En el bloque de tests de POST /api/uploads/guest/presigned-url falta
cubrir el flujo de rate limiting, así que añade un caso que fuerce rateLimitMock
a devolver blocked true con retryAfterSeconds y verifique que POST responde 429
y setea el header Retry-After; usa los mismos helpers como makeRequest, POST,
rateLimitMock y recordAttemptMock para también confirmar que recordAttemptMock
no se llama cuando la petición es bloqueada.
app/api/uploads/guest/presigned-url/route.ts (1)

47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

recordLoginAttempt reutilizado para uploads acopla conceptos no relacionados.

Registrar una "subida exitosa de invitado" mediante un método llamado recordLoginAttempt confunde el modelo del rate limiter. Considerar un método de propósito genérico (p. ej. recordAttempt/recordSuccess) para no mezclar la semántica de login con la de uploads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/uploads/guest/presigned-url/route.ts` at line 47, The upload success
path is using the login-specific rate limiter API, which mixes authentication
semantics with guest upload tracking. Update the guest presigned URL handler to
call a generic rate limiter method instead of `recordLoginAttempt`, and add or
rename the corresponding method on `rateLimiter` to something purpose-neutral
like `recordAttempt` or `recordSuccess`. Keep the upload flow in
`app/api/uploads/guest/presigned-url/route.ts` aligned with the new generic
method name so the intent is clear across the uploader and limiter code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`[locale]/products/[id]/customization-draft-context.tsx:
- Around line 133-139: The image URL validation in validateDraft is too strict
and currently rejects blob: URLs created by customization-form.tsx via
URL.createObjectURL(file), which breaks preview in CustomizationPreview. Update
the check in customization-draft-context.tsx so validateDraft accepts blob: (and
data: if intended) in addition to http(s):// and root-relative paths, while
keeping the existing error assignment logic for truly invalid URLs.

In `@app/`[locale]/products/[id]/customization-form.tsx:
- Around line 166-181: The upload flow in customization-form’s image handler is
doing redundant state updates and leaking object URLs; update the
onUpload/onUploaded flow so image state is set in only one place, verify the PUT
request to result.uploadUrl succeeds before creating the preview URL or
returning uploadResult, and make sure the blob URL from URL.createObjectURL is
revoked when the image is replaced or cleared. Use the existing upload callback
and setImage logic in customization-form.tsx to locate the fix.

In `@app/`[locale]/products/[id]/customization-preview.tsx:
- Around line 84-93: The preview image rendered in customization-preview.tsx
uses draft.imageUrl from URL.createObjectURL(file), so next/image may fail on
blob: URLs at runtime. Update the Image usage in the customization preview to
either add the unoptimized prop or replace it with a plain img, and make sure
the fix is applied at the draft.imageUrl rendering block inside the
customization preview component.

In `@app/`[locale]/products/[id]/page.module.css:
- Around line 14-220: The CSS in this module has the same Stylelint/Prettier
violations as the other product styles, mainly legacy alpha/color-function
notation and non-modern media query ranges. Update the affected selectors and
rules such as .grid, .visualCard, .badge, and the `@media` blocks to use modern
notation like rgba()/transparent handling consistent with the rest of the
codebase and replace range queries with the current syntax (for example,
width-based comparisons). Also run Prettier formatting on the stylesheet so the
output matches CI expectations; keep the camelCase CSS module class names
unchanged despite selector-class-pattern warnings.

In `@app/`[locale]/products/[id]/page.tsx:
- Around line 89-98: The product page still contains hardcoded English strings
instead of using the locale dictionary. Update the text in the main
preview/visual section in the page component and the label strings used by the
add-to-cart flow in the same component to come from `dict`, matching the
existing i18n pattern. Keep the relevant symbols in mind in the page component,
including the preview/visual markup and the add state labels (`adding`, `added`,
`error`), so the view stays consistent for `es` and `cat`.

In `@app/`[locale]/products/[id]/photo-upload-field.tsx:
- Around line 55-59: The upload flow in photo-upload-field’s async handler is
not tied to the actual promise lifecycle, so loading state can end early and
rejections are left unhandled. Update the handler around
startTransition/onUpload so the transition tracks the full async operation,
await or otherwise chain the onUpload(file) promise directly, and add explicit
rejection handling to surface an error state/message to the user. Keep the
onUploaded callback only on success and ensure the loading flag stays aligned
with the onUpload promise in this component.

In `@app/`[locale]/products/[id]/preview-overlay.module.css:
- Around line 12-52: Los fallos están en `preview-overlay.module.css`:
Stylelint/Prettier rechazan las notaciones de color y el formato actual.
Actualiza las declaraciones de color en `baseImage`, `textLayer`, `photoLayer` y
`previewShell::before` a la sintaxis moderna compatible con las reglas (`rgb(...
/ NN%)` o equivalente), y deja el archivo con el formato que espera Prettier. No
toques los nombres camelCase de las clases como `previewShell` o `photoLayer`,
porque el problema de `selector-class-pattern` parece ser de configuración
global y no de este módulo.

In `@app/`[locale]/seller/products/page.module.css:
- Around line 14-34: The CSS module has class names in page.module.css that will
fail selector-class-pattern because headerActions and rowActions are camelCase
while the rule expects kebab-case. Update the styling approach consistently by
either renaming these selectors and fixing the corresponding references in
page.tsx, or by changing the Stylelint selector-class-pattern rule to allow the
existing camelCase convention used by searchWrap and pageButton. Keep the chosen
convention consistent across the module and lint config.

In `@app/api/cart/migrate/route.ts`:
- Around line 56-66: The cart migration flow resolves
`customizationImageUploadId` with `uploadRepository.findById` and then
immediately calls `storagePort.generateReadUrl`, so add an ownership check in
this path before issuing the URL. In `route.ts`, after loading the upload and
before generating the read URL, verify that `upload.uploadedBy` matches the
current session user; if it does not, reject the request with an appropriate
authorization error. Keep the check close to the existing `findById` /
`generateReadUrl` logic so the secure path is enforced for `item` uploads.

In `@docker-compose.e2e.yml`:
- Line 40: La base URL semilla está apuntando a localhost dentro del contenedor
app, así que las imágenes generadas por prisma/seed-data.ts quedan inaccesibles
desde server-side. Cambia SEED_PRODUCT_ASSET_BASE_URL en docker-compose.e2e.yml
para que use el servicio product-assets como host, manteniendo esa referencia
consistente con el flujo de npx prisma db seed y las cargas de Next.

In `@modules/products/domain/value-objects/product-customization-config.ts`:
- Around line 106-108: The allowsText() helper in ProductCustomizationConfig is
too broad because it only excludes photo, so description still returns true and
lets CreateCustomerCustomization.assertAllowed() accept dto.text incorrectly.
Update allowsText() to reflect the valid text-capable modes explicitly, and make
sure it returns false for description as well as photo so text is only permitted
when customization is actually enabled.

In `@modules/products/infrastructure/mapper.ts`:
- Around line 179-182: The toPersistenceProduct mapping currently omits
customizationConfig when it is default, so PrismaProductRepository.update() can
leave stale JSON behind on products switching back to the default configuration.
Update the toPersistenceProduct logic in the product mapper to explicitly set
persistence.customizationConfig to null when product.customizationConfig is
missing or isDefault(), and only serialize toJson() for non-default configs.

In `@modules/products/infrastructure/prisma-product-repository.ts`:
- Around line 177-212: The `update` flow in `PrismaProductRepository` now always
returns `true`, but `tx.product.update` will throw Prisma `P2025` when the
product is missing, so the repository no longer preserves the old “not found”
contract. Update the `update` method to either catch that `P2025` case and
return `false`, or align the caller expectations by removing the downstream
`persisted` check in `UpdateProductUseCase`; keep the behavior consistent with
the existing `update` and `findById` flow so a missing product is handled
cleanly.

In `@next.config.ts`:
- Around line 22-25: The remotePatterns entry is too permissive because
next.config.ts currently allows https requests from any hostname, which leaves
the image optimizer open to arbitrary external hosts. Update the remotePatterns
configuration to whitelist only the actual CDN or bucket hostnames used by the
app, and tighten pathname as needed; keep the change localized to the images
config in next.config.ts.

In `@package.json`:
- Around line 7-9: The dev:full workflow can still race Postgres startup because
dev:env only brings up db/product-assets and does not ensure the database is
ready before prisma db push runs. Update the docker compose setup for the db
service by adding a proper healthcheck, then change the dev:env or dev:full flow
to use docker compose up --wait (or equivalent readiness polling) so prisma db
push and prisma db seed only run after the db service is healthy.

In `@tests/e2e/products/products.spec.ts`:
- Around line 21-35: The product detail test in products.spec.ts is relying on
the first anchor matching /products/, which is fragile because the order is not
guaranteed and may navigate to the wrong item. Update the test to target the
specific “Camiseta Personalizada” link in the product listing, or otherwise use
its explicit href, and keep the rest of the assertions in the same flow using
the existing page locator and page.goto logic.

In
`@tests/unit/modules/products/domain/value-objects/product-customization-config.test.ts`:
- Around line 5-15: The default ProductCustomizationConfig test only checks
preview-related fields, so it misses the capability matrix for the default
description mode. Update the ProductCustomizationConfig.default() test in
product-customization-config.test.ts to assert that the description mode rejects
text, photo, and style capabilities by exercising allowsText(), the
photo-related allowance, and the style-related allowance on the config instance.
This should catch the current allowsText() regression while keeping the existing
default-state assertions in place.

---

Outside diff comments:
In `@app/`[locale]/cart/cart-view.tsx:
- Around line 267-283: The customization block in cart-view.tsx can render an
empty span when only imageUrl is present because the visibility check includes
item.customization.imageUrl but the content only formats size, color, and text.
Update the conditional around the customization <span> in CartView so it only
depends on fields actually rendered there, and keep imageUrl handling separate
since it is already rendered elsewhere. Use the existing item.customization
checks and labels.customizationSize/customizationColor/customizationText as the
reference points when adjusting the condition.

In `@modules/cart/application/migrate-guest-cart.ts`:
- Around line 119-202: Avoid creating guest customizations before the
keep-server branch is resolved. In migrate-guest-cart,
resolveGuestCustomizationIds is currently called while building availableGuest,
so it can persist new customizations even when dto.strategy is keep-server and
the existing cart is left unchanged. Move the customization resolution into the
branches that actually build or merge items (or guard it so keep-server with a
non-new cart skips creation), using migrate-guest-cart,
resolveGuestCustomizationIds, availableGuest, and the dto.strategy checks as the
key locations.

---

Nitpick comments:
In `@app/`[locale]/cart/cart-view.tsx:
- Around line 284-302: The cart item edit link in cart-view.tsx calls
buildCustomizationHref multiple times per item, recreating URLSearchParams
unnecessarily. Compute the customization URL once before the JSX in the cart
item render path, store it in a local variable, and reuse that value for both
the condition and the href in the edit link. Keep the fix centered around
buildCustomizationHref and the cart item rendering logic.

In `@app/`[locale]/seller/products/new/page.tsx:
- Around line 24-42: The labels mapping in the product create page is duplicated
with the edit page and can drift over time. Extract the shared label
construction into a helper such as buildProductFormLabels(dict) and reuse it
from both the create and edit page components, while keeping only the
page-specific title/save overrides in the respective page files.

In `@app/`[locale]/seller/products/product-form.tsx:
- Around line 273-275: The submit button in the product form is showing the same
text in both states because submitLabel matches labels.save; update the button
rendering in product-form so it uses a distinct loading label while loading is
true and the normal submitLabel only when idle. Use the existing button state in
the form component to switch between a “saving” message and the default submit
text, so the user gets clear feedback during submission.

In `@app/api/cart/migrate/route.ts`:
- Around line 73-120: The cart migration flow is doing sequential N+1 product
lookups twice via `productsModuleRepo.findById` when building
`capabilityProductMap` and `productMap`, and the same products may be fetched
again. Update `MigrateGuestCart` usage in this route to batch-load product data
if `productsModuleRepo` exposes a `findManyByIds`/similar method, then populate
both maps from that shared result set. If the ID sets overlap, reuse the already
loaded entities instead of querying them again.

In `@app/api/customizations/customer/route.ts`:
- Around line 30-41: The `getConfig` method in `capabilityPort` contains an
unreachable `productId !== product.id` branch because `assertAllowed` only calls
it with the current product’s id. Remove that branch and the extra
`productRepository.findById` lookup, and have `getConfig` return the already
loaded `product.customizationConfig` or `ProductCustomizationConfig.default()`
directly.

In `@app/api/uploads/guest/presigned-url/route.ts`:
- Line 47: The upload success path is using the login-specific rate limiter API,
which mixes authentication semantics with guest upload tracking. Update the
guest presigned URL handler to call a generic rate limiter method instead of
`recordLoginAttempt`, and add or rename the corresponding method on
`rateLimiter` to something purpose-neutral like `recordAttempt` or
`recordSuccess`. Keep the upload flow in
`app/api/uploads/guest/presigned-url/route.ts` aligned with the new generic
method name so the intent is clear across the uploader and limiter code.

In `@components/cart/add-to-cart-button.tsx`:
- Around line 133-208: The customization validation logic is duplicated in both
the authenticated and guest branches, so move the shared
normalizeCustomizationDraft plus customizationDraftSchema.safeParse check
(including the error state handling) to a single place before the
isAuthenticated conditional in add-to-cart-button.tsx. Keep the existing
success/failure behavior the same, but have both branches reuse the already
validated normalizedCustomization instead of repeating the same block.

In `@modules/customizations/presentation/schemas/customization-schemas.ts`:
- Around line 34-38: `createCustomerCustomizationSchema` is an exact duplicate
of `createCustomizationSchema`; refactor it to reuse the existing schema instead
of rebuilding the same `customizationFieldsSchema.extend(...).strict()` chain.
Locate the duplicated schema definitions in `customization-schemas.ts` and make
`createCustomerCustomizationSchema` reference the shared
`createCustomizationSchema` (or extract a common base helper) so both stay in
sync with future changes.

In `@modules/products/presentation/schemas/product-form-schema.ts`:
- Line 30: Update the product form schema to stop using the deprecated Zod
helper: in product-form-schema.ts, replace the ProductStatus validation in the
schema definition with z.enum(...) instead of z.nativeEnum(...), keeping the
field optional and preserving the existing ProductStatus values. Use the schema
object in the product form schema as the place to make the change.

In `@prisma/seed.ts`:
- Around line 122-124: The product seed log in the create block is using
customizationConfig.previewEnabled to print the “customizable” label, which
misrepresents products like those with mode set to description. Update the
console.log in prisma/seed.ts near the product creation flow to either derive
the label from customizationConfig.mode instead of previewEnabled, or rename the
label to “preview” so it matches the field being reported; use the existing
getSeedProductLabel(p) and product.id context to keep the message consistent.

In `@tests/unit/app/api/uploads/guest/presigned-url/route.test.ts`:
- Around line 54-83: En el bloque de tests de POST
/api/uploads/guest/presigned-url falta cubrir el flujo de rate limiting, así que
añade un caso que fuerce rateLimitMock a devolver blocked true con
retryAfterSeconds y verifique que POST responde 429 y setea el header
Retry-After; usa los mismos helpers como makeRequest, POST, rateLimitMock y
recordAttemptMock para también confirmar que recordAttemptMock no se llama
cuando la petición es bloqueada.

In `@tests/unit/prisma/prisma-seed-config.test.ts`:
- Around line 28-34: The test in prisma-seed-config.test.ts is brittle because
it asserts against literal source text instead of behavior. Update the seed test
to verify sequential writes by exercising the seed logic with a mocked
Prisma/client layer and asserting the write calls happen in order, rather than
checking for Promise.all or exact loop text. Use the seed-related symbols like
seedPath and the seeding routine under test to locate the behavior and keep the
assertion resilient to harmless refactors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ebc0db28-2fbf-4cea-9195-8f1a7aca18a0

📥 Commits

Reviewing files that changed from the base of the PR and between 8f59119 and 2e20299.

⛔ Files ignored due to path filters (3)
  • devresources/products/taza.png is excluded by !**/*.png
  • public/img/products/customizable-hoodie.svg is excluded by !**/*.svg
  • public/img/products/customizable-tshirt.svg is excluded by !**/*.svg
📒 Files selected for processing (79)
  • .gitignore
  • AGENTS.md
  • app/[locale]/cart/cart-view.tsx
  • app/[locale]/cart/page.tsx
  • app/[locale]/checkout/page.tsx
  • app/[locale]/products/[id]/customization-draft-context.tsx
  • app/[locale]/products/[id]/customization-experience.tsx
  • app/[locale]/products/[id]/customization-form.tsx
  • app/[locale]/products/[id]/customization-preview.tsx
  • app/[locale]/products/[id]/page.module.css
  • app/[locale]/products/[id]/page.tsx
  • app/[locale]/products/[id]/photo-upload-field.tsx
  • app/[locale]/products/[id]/preview-overlay.module.css
  • app/[locale]/seller/products/[id]/edit/page.tsx
  • app/[locale]/seller/products/new/page.tsx
  • app/[locale]/seller/products/page.module.css
  • app/[locale]/seller/products/page.tsx
  • app/[locale]/seller/products/product-form.tsx
  • app/api/cart/migrate/route.ts
  • app/api/cart/route.ts
  • app/api/customizations/customer/route.ts
  • app/api/products/[id]/route.ts
  • app/api/products/route.ts
  • app/api/uploads/guest/presigned-url/route.ts
  • components/cart/add-to-cart-button.tsx
  • components/cart/customization-draft-schema.ts
  • docker-compose.e2e.yml
  • docker-compose.yml
  • modules/cart/application/migrate-guest-cart.ts
  • modules/cart/domain/customization-create-port.ts
  • modules/cart/presentation/guest-cart-context.tsx
  • modules/cart/presentation/schemas/cart-schemas.ts
  • modules/customizations/application/create-customer-customization.ts
  • modules/customizations/presentation/schemas/customization-schemas.ts
  • modules/orders/application/handle-cart-checked-out.ts
  • modules/orders/infrastructure/product-repository-adapter.ts
  • modules/products/application/create-product-use-case.ts
  • modules/products/application/update-product-use-case.ts
  • modules/products/domain/entities/product.ts
  • modules/products/domain/product-capability-port.ts
  • modules/products/domain/value-objects/product-customization-config.ts
  • modules/products/infrastructure/mapper.ts
  • modules/products/infrastructure/prisma-product-repository.ts
  • modules/products/presentation/product-response.ts
  • modules/products/presentation/schemas/product-form-schema.ts
  • modules/uploads/presentation/schemas/upload-schemas.ts
  • next.config.ts
  • package.json
  • prisma/migrations/20260629160000_add_product_customization_config_and_upload_type/migration.sql
  • prisma/schema.prisma
  • prisma/seed-data.ts
  • prisma/seed.ts
  • shared/i18n/locales/cat.json
  • shared/i18n/locales/es.json
  • shared/infrastructure/prisma.ts
  • tests/e2e/products/products.spec.ts
  • tests/unit/app/[locale]/cart/cart-view-customization.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-form.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-preview.test.tsx
  • tests/unit/app/[locale]/products/[id]/photo-upload-field-status.test.tsx
  • tests/unit/app/[locale]/products/[id]/photo-upload-field.test.tsx
  • tests/unit/app/[locale]/seller/products/page.test.tsx
  • tests/unit/app/[locale]/seller/products/product-form.test.tsx
  • tests/unit/app/api/cart/migrate/route.test.ts
  • tests/unit/app/api/customizations/customer/route.test.ts
  • tests/unit/app/api/products/[id]/route.test.ts
  • tests/unit/app/api/products/route.test.ts
  • tests/unit/app/api/uploads/guest/presigned-url/route.test.ts
  • tests/unit/components/cart/add-to-cart-button.test.tsx
  • tests/unit/modules/cart/application/migrate-guest-cart.test.ts
  • tests/unit/modules/customizations/application/create-customer-customization.test.ts
  • tests/unit/modules/products/application/create-product-use-case.test.ts
  • tests/unit/modules/products/application/update-product-use-case.test.ts
  • tests/unit/modules/products/domain/value-objects/product-customization-config.test.ts
  • tests/unit/modules/products/infrastructure/prisma-product-repository.test.ts
  • tests/unit/modules/products/infrastructure/product-customization-config-mapper.test.ts
  • tests/unit/prisma/prisma-seed-config.test.ts
  • tests/unit/prisma/seed-data.test.ts

Comment on lines +133 to +139
if (
imageUrl &&
!/^https?:\/\//.test(imageUrl) &&
!imageUrl.startsWith('/')
) {
nextErrors.imageUrl = 'Customization image must be a valid URL.';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

validateDraft rechaza las imágenes subidas (blob URLs), rompiendo la previsualización.

Tras subir una foto, customization-form.tsx asigna imageUrl = URL.createObjectURL(file), que produce una URL blob:. La validación solo acepta URLs que empiezan por http(s):// o /, por lo que una blob: URL fallará ambas condiciones y marcará errors.imageUrl. Esto deja draft.error activo y desactiva canPreview en CustomizationPreview, justo en el flujo de personalización con foto. Acepta también el esquema blob: (y data: si aplica).

🐛 Posible corrección
     if (
       imageUrl &&
       !/^https?:\/\//.test(imageUrl) &&
+      !imageUrl.startsWith('blob:') &&
       !imageUrl.startsWith('/')
     ) {
       nextErrors.imageUrl = 'Customization image must be a valid URL.';
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
imageUrl &&
!/^https?:\/\//.test(imageUrl) &&
!imageUrl.startsWith('/')
) {
nextErrors.imageUrl = 'Customization image must be a valid URL.';
}
if (
imageUrl &&
!/^https?:\/\//.test(imageUrl) &&
!imageUrl.startsWith('blob:') &&
!imageUrl.startsWith('/')
) {
nextErrors.imageUrl = 'Customization image must be a valid URL.';
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-draft-context.tsx around lines 133
- 139, The image URL validation in validateDraft is too strict and currently
rejects blob: URLs created by customization-form.tsx via
URL.createObjectURL(file), which breaks preview in CustomizationPreview. Update
the check in customization-draft-context.tsx so validateDraft accepts blob: (and
data: if intended) in addition to http(s):// and root-relative paths, while
keeping the existing error assignment logic for truly invalid URLs.

Comment thread app/[locale]/products/[id]/customization-form.tsx Outdated
Comment thread app/[locale]/products/[id]/customization-preview.tsx
Comment thread app/[locale]/products/[id]/page.module.css Outdated
Comment thread app/[locale]/products/[id]/page.tsx Outdated
Comment on lines +177 to +212
await prisma.$transaction(async (tx) => {
await tx.product.update({
where: { id: entity.id },
data: {
basePrice: data.basePrice,
currency: data.currency,
status: data.status,
categoryId: data.categoryId,
customizationConfig: data.customizationConfig,
updatedAt: data.updatedAt,
},
});

for (const translation of entity.translations) {
await tx.productTranslation.upsert({
where: {
productId_locale: {
productId: entity.id,
locale: translation.locale,
},
},
create: {
productId: entity.id,
locale: translation.locale,
name: translation.name,
description: translation.description,
},
update: {
name: translation.name,
description: translation.description,
},
});
}
});
return result !== null;

return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

El update ahora siempre devuelve true, cambiando el contrato de "no encontrado".

Antes el booleano de retorno señalaba si el producto existía. Ahora la función siempre retorna true, y si el producto no existe, tx.product.update lanzará un error Prisma P2025 en lugar de devolver false. Esto convierte en código muerto la comprobación if (!persisted) de UpdateProductUseCase (líneas 104-107) y, en una ventana de carrera (producto eliminado entre findById y update), provocará un error genérico (500) en vez de un 404 limpio.

Considera capturar P2025 y devolver false, o documentar/eliminar la comprobación aguas abajo para mantener la coherencia.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/infrastructure/prisma-product-repository.ts` around lines
177 - 212, The `update` flow in `PrismaProductRepository` now always returns
`true`, but `tx.product.update` will throw Prisma `P2025` when the product is
missing, so the repository no longer preserves the old “not found” contract.
Update the `update` method to either catch that `P2025` case and return `false`,
or align the caller expectations by removing the downstream `persisted` check in
`UpdateProductUseCase`; keep the behavior consistent with the existing `update`
and `findById` flow so a missing product is handled cleanly.

Comment thread next.config.ts
Comment on lines +22 to +25
{
protocol: 'https',
hostname: '**',
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map repository files of interest
git ls-files | rg '(^|/)next\.config\.ts$|(^|/)next\.config\.(js|mjs|cjs|ts)$|(^|/)package\.json$|(^|/)README\.md$'

# Show the relevant config file with line numbers
if [ -f next.config.ts ]; then
  echo '--- next.config.ts ---'
  cat -n next.config.ts
fi

# Find image configuration usage and remotePatterns references
rg -n "remotePatterns|images\s*:|hostname:\s*'\*\*'|hostname:\s*\"\*\*\"" -S .

Repository: henri318/728-store

Length of output: 5341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact config shape and any env-gated logic around image domains
if [ -f next.config.ts ]; then
  sed -n '1,220p' next.config.ts | cat -n
fi

Repository: henri318/728-store

Length of output: 1217


🌐 Web query:

Next.js remotePatterns hostname '**' any host open proxy image optimization documentation

💡 Result:

In Next.js, using remotePatterns with hostname: '' allows your Image Optimization API to fetch and optimize images from any external host [1][2][3]. While this enables support for arbitrary or unknown image sources, it introduces significant security risks, primarily Server-Side Request Forgery (SSRF) [1][4][5]. Security Risks When remotePatterns is configured with a wildcard such as '', the Next.js image optimizer effectively becomes an open proxy [1][4]. An attacker can pass any URL to the next/image component to force your server to make outgoing requests to arbitrary destinations [4][5]. This can be exploited to: - Conduct internal network reconnaissance [5]. - Access sensitive internal services or metadata endpoints that are not publicly exposed [5]. - Bypass IP-based access controls [1][5]. Official Recommendation and Best Practices The official Next.js documentation and security guidelines strongly recommend against using broad wildcards for the hostname [6][7]. To secure your application: 1. Whitelist Trusted Domains: Explicitly define only the specific domains you need to load images from [1][5]. 2. Use Granular Patterns: If you must support many subdomains, use specific patterns like .example.com instead of `` [7]. 3. Restrict by Pathname: Further limit the scope by defining specific pathname patterns (e.g., `/images/`) to prevent access to unauthorized directories on a trusted host [8][5][7]. 4. Use `unoptimized`: If your application must handle images from completely arbitrary and untrusted sources, consider setting `images.unoptimized: true` in your `next.config.js` [9]. This disables the built-in Image Optimization API, shifting the image loading directly to the client browser and preventing your server from performing the fetch [9]. Implementation Example For specific, secure configuration: // next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.trusted-example.com', port: '', pathname: '/public/', }, ], }, }; If you require images from any host without risking SSRF, opt for the unoptimized approach: // next.config.js module.exports = { images: { unoptimized: true, }, };

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for external image URL sources and any documentation that explains the intended allowlist
rg -n "https?://|remotePatterns|images:" -S README.md docs modules tests prisma next.config.ts

Repository: henri318/728-store

Length of output: 20449


🌐 Web query:

site:nextjs.org remotePatterns hostname '**' Next.js images any host wildcard security

💡 Result:

Using a wildcard hostname like '' in the remotePatterns configuration of your next.config.js is highly insecure and is strongly discouraged [1][2][3]. The remotePatterns configuration is designed to act as a security whitelist for the Next.js Image Optimization API [1][2]. By default, Next.js requires strict configuration of protocols, hostnames, ports, and pathnames to prevent malicious actors from exploiting your image optimization server [4][1][2]. Security implications of using '' as a hostname: 1. Open Proxy Risk: Configuring a wildcard hostname allows your application to fetch and optimize images from any arbitrary URL on the internet. Attackers can use your server as a proxy to obscure their traffic, perform server-side request forgery (SSRF), or exhaust your server resources by forcing it to process large or malformed images [1][2][3]. 2. Bypassing Restrictions: The entire purpose of remotePatterns is to restrict access to trusted sources (such as your own CDN or S3 bucket) [1][2]. A wildcard essentially disables this security boundary, rendering the protection provided by remotePatterns ineffective [1][2]. Official Recommendation: You should always be as specific as possible in your configuration [5][6][7]. Instead of using wildcards, define exact hostnames and paths [4]. For example, specify the exact domain and, if necessary, use wildcards only for specific sub-paths (e.g., pathname: '/my-bucket/') rather than the hostname itself [5][1][2]. If you need to support multiple specific domains, add each one as a separate object in the remotePatterns array [1][2].

Citations:


Restringe remotePatterns a orígenes concretos

hostname: '**' en https deja /_next/image abierto a cualquier host externo, ampliando la superficie de SSRF y el abuso de recursos. Limítalo a los CDN/buckets reales y, si hace falta, acota también pathname.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@next.config.ts` around lines 22 - 25, The remotePatterns entry is too
permissive because next.config.ts currently allows https requests from any
hostname, which leaves the image optimizer open to arbitrary external hosts.
Update the remotePatterns configuration to whitelist only the actual CDN or
bucket hostnames used by the app, and tighten pathname as needed; keep the
change localized to the images config in next.config.ts.

Comment thread package.json
Comment on lines +7 to +9
"dev:env": "docker compose up -d db product-assets",
"dev:full": "npm run dev:env && prisma db push && prisma db seed && npm run dev",
"dev:down": "docker compose down",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verificar si el servicio db define un healthcheck (requerido por `--wait`)
fd -t f 'docker-compose*.yml' --exec sh -c 'echo "== {} =="; cat {}'

Repository: henri318/728-store

Length of output: 649


🌐 Web query:

docker compose up --wait requires healthcheck services docs

💡 Result:

The docker compose up --wait command does not strictly require a healthcheck to be defined for your services, but its behavior changes depending on whether one is configured [1][2]. When you run docker compose up --wait, Compose waits for all selected services to reach a state of either running or healthy [3][4][5]. The specific behavior is as follows: 1. Services with a healthcheck: Compose will wait until the service reports a healthy status [2][6]. 2. Services without a healthcheck: Compose considers the service ready as soon as the container has successfully started (is in a running state) [1][2]. In this context, if no healthcheck is defined, the running state serves as the minimum "readiness" check [1]. While the command does not fail if a healthcheck is missing, defining one is recommended for services where "running" does not equate to "ready to accept traffic" (e.g., databases or applications performing initialization tasks) [7][8]. You can also use the --wait-timeout flag to specify a maximum duration (in seconds) to wait for services to reach the healthy or running state [3][4][5]. If the timeout is reached before services are ready, the command will exit with an error [1].

Citations:


dev:full necesita esperar a que Postgres esté listo. docker compose up --wait aquí solo espera a que el contenedor esté en ejecución, porque db no tiene healthcheck; así prisma db push puede seguir fallando en arranques en frío. Añade un healthcheck al servicio db y usa --wait, o introduce un sondeo de readiness antes de prisma db push.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 7 - 9, The dev:full workflow can still race
Postgres startup because dev:env only brings up db/product-assets and does not
ensure the database is ready before prisma db push runs. Update the docker
compose setup for the db service by adding a proper healthcheck, then change the
dev:env or dev:full flow to use docker compose up --wait (or equivalent
readiness polling) so prisma db push and prisma db seed only run after the db
service is healthy.

Comment on lines +21 to +35
const detailsHref = await page
.locator('a[href*="/products/"]')
.first()
.click();
.getAttribute('href');
expect(detailsHref).toBeTruthy();

await expect(page).toHaveURL(/\/es\/products\//);
await page.goto(detailsHref!);

await expect(page.locator('h1').first()).toBeVisible();
await expect(
page.getByRole('img', { name: 'Camiseta Personalizada' }),
).toBeVisible();
await expect(
page.getByText('Vista previa de personalización'),
).toBeVisible();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verificar el orden/markup de los enlaces de producto en la home y los nombres accesibles de las imágenes
fd -t f 'seed-data.ts' prisma --exec cat -n {}
rg -nP "products/\[id\]" app -g '*.tsx' -C2

Repository: henri318/728-store

Length of output: 5902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant test and product-related source files, then inspect the exact selectors/ordering.
git ls-files 'tests/e2e/products/products.spec.ts' 'app/**' 'src/**' 'prisma/**' | sed -n '1,200p'

printf '\n== products.spec.ts ==\n'
cat -n tests/e2e/products/products.spec.ts

printf '\n== product-related files (names only) ==\n'
fd -t f -i 'product' app src prisma tests | sed -n '1,200p'

printf '\n== routes/selectors around product links ==\n'
rg -n -C 3 "href=.*?/products/|/products/\\[|getByRole\\('img'|getByText\\('Vista previa de personalización'|Camiseta Personalizada|Taza Personalizada|Sudadera con Capucha" app src tests -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx'

printf '\n== AST outlines for likely page/component files ==\n'
for f in $(fd -t f -i 'products' app src tests | head -n 20); do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded 2>/dev/null | sed -n '1,120p' || true
done

Repository: henri318/728-store

Length of output: 20775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the product listing page and the e2e test to see whether the first /products/ link is deterministic.
rg -n -C 4 "a href|Link|href=.*?/products/|Camiseta Personalizada|Taza Personalizada|Sudadera con Capucha|Vista previa de personalización" app src tests -g '*.tsx' -g '*.ts'

printf '\n== test file ==\n'
cat -n tests/e2e/products/products.spec.ts

Repository: henri318/728-store

Length of output: 47118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the product listing/home page and the product detail route used by the test.
rg -n -C 4 "href=.*?/products/|Link.*?/products/|getByRole\\('img'|Vista previa de personalización|Camiseta Personalizada|Taza Personalizada|Sudadera con Capucha" app src tests -g '*.tsx' -g '*.ts'

printf '\n== e2e test ==\n'
cat -n tests/e2e/products/products.spec.ts

Repository: henri318/728-store

Length of output: 7003


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== tests/e2e/products/products.spec.ts ==\n'
cat -n tests/e2e/products/products.spec.ts

printf '\n== all /products/ links in app/src/tests ==\n'
rg -n -C 2 'href=.*?/products/|to=.*?/products/|/products/' app src tests -g '*.tsx' -g '*.ts' | sed -n '1,220p'

Repository: henri318/728-store

Length of output: 24038


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the product list/detail UI files by filename, then inspect only the relevant sections.
git ls-files | rg '(^|/)(page|layout|product|products)\.(tsx|ts|jsx|js)$|tests/e2e/products/products\.spec\.ts$' | sed -n '1,200p'

printf '\n== products.spec.ts ==\n'
cat -n tests/e2e/products/products.spec.ts

printf '\n== candidate product UI files outline ==\n'
for f in $(git ls-files | rg '(^|/)(page|layout|product|products)\.(tsx|ts|jsx|js)$'); do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded 2>/dev/null | sed -n '1,160p' || true
done

Repository: henri318/728-store

Length of output: 8081


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== app/[locale]/page.tsx ==\n'
cat -n app/[locale]/page.tsx

printf '\n== product list use case ==\n'
fd -t f 'product-list-query-use-case.ts' modules tests app
echo
cat -n modules/products/application/product-list-query-use-case.ts

printf '\n== product repository interfaces/implementations with ordering ==\n'
rg -n -C 3 "orderBy|sort|createdAt|updatedAt|list\\(" modules/products tests -g '*.ts' -g '*.tsx'

Repository: henri318/728-store

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== app/[locale]/page.tsx ==\n'
cat -n app/[locale]/page.tsx

printf '\n== product list use case ==\n'
cat -n modules/products/application/product-list-query-use-case.ts

printf '\n== product repository ordering/search ==\n'
rg -n -C 3 "orderBy|sort|createdAt|updatedAt|list\\(" modules/products tests -g '*.ts' -g '*.tsx'

Repository: henri318/728-store

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== prisma-product-repository.ts (findAll + findPaginated) ==\n'
cat -n modules/products/infrastructure/prisma-product-repository.ts | sed -n '1,130p'

printf '\n== memory-product-repository.ts ==\n'
cat -n tests/doubles/memory-product-repository.ts | sed -n '1,220p'

printf '\n== integration tests around findAll ordering ==\n'
rg -n -C 3 "findAll\\(|returns .*findAll|sorted|orderBy: \\{ createdAt|orderBy: \\{.*createdAt" tests/integration/products/prisma-product-repository.integration.test.ts tests/unit/modules/products/infrastructure/prisma-product-repository.test.ts -g '*.ts'

Repository: henri318/728-store

Length of output: 12342


Selector frágil en el detalle de producto: findAll() no fija orden y .first() puede apuntar a otro producto; localiza el enlace de "Camiseta Personalizada" o usa su href explícito en lugar de depender del primer /products/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/products/products.spec.ts` around lines 21 - 35, The product detail
test in products.spec.ts is relying on the first anchor matching /products/,
which is fragile because the order is not guaranteed and may navigate to the
wrong item. Update the test to target the specific “Camiseta Personalizada” link
in the product listing, or otherwise use its explicit href, and keep the rest of
the assertions in the same flow using the existing page locator and page.goto
logic.

Comment on lines +5 to +15
it('defaults to description-only with no preview', () => {
const config = ProductCustomizationConfig.default();

expect(config.mode).toBe('description');
expect(config.previewEnabled).toBe(false);
expect(config.previewTemplateUrl).toBeNull();
expect(config.textOffset).toBeNull();
expect(config.imageOffset).toBeNull();
expect(config.isDefault()).toBe(true);
expect(config.isPreviewCapable()).toBe(false);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Falta cubrir la matriz de capacidades del modo por defecto.

La suite no comprueba que description rechace texto, foto y estilos. Con esa regresión, el fallo actual de allowsText() pasa desapercibido.

Propuesta de cambio
   it('defaults to description-only with no preview', () => {
     const config = ProductCustomizationConfig.default();

     expect(config.mode).toBe('description');
     expect(config.previewEnabled).toBe(false);
     expect(config.previewTemplateUrl).toBeNull();
     expect(config.textOffset).toBeNull();
     expect(config.imageOffset).toBeNull();
+    expect(config.allowsText()).toBe(false);
+    expect(config.allowsPhoto()).toBe(false);
+    expect(config.allowsStyleOptions()).toBe(false);
     expect(config.isDefault()).toBe(true);
     expect(config.isPreviewCapable()).toBe(false);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('defaults to description-only with no preview', () => {
const config = ProductCustomizationConfig.default();
expect(config.mode).toBe('description');
expect(config.previewEnabled).toBe(false);
expect(config.previewTemplateUrl).toBeNull();
expect(config.textOffset).toBeNull();
expect(config.imageOffset).toBeNull();
expect(config.isDefault()).toBe(true);
expect(config.isPreviewCapable()).toBe(false);
});
it('defaults to description-only with no preview', () => {
const config = ProductCustomizationConfig.default();
expect(config.mode).toBe('description');
expect(config.previewEnabled).toBe(false);
expect(config.previewTemplateUrl).toBeNull();
expect(config.textOffset).toBeNull();
expect(config.imageOffset).toBeNull();
expect(config.allowsText()).toBe(false);
expect(config.allowsPhoto()).toBe(false);
expect(config.allowsStyleOptions()).toBe(false);
expect(config.isDefault()).toBe(true);
expect(config.isPreviewCapable()).toBe(false);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/unit/modules/products/domain/value-objects/product-customization-config.test.ts`
around lines 5 - 15, The default ProductCustomizationConfig test only checks
preview-related fields, so it misses the capability matrix for the default
description mode. Update the ProductCustomizationConfig.default() test in
product-customization-config.test.ts to assert that the description mode rejects
text, photo, and style capabilities by exercising allowsText(), the
photo-related allowance, and the style-related allowance on the config instance.
This should catch the current allowsText() regression while keeping the existing
default-state assertions in place.

Source: Learnings

@henri318
henri318 marked this pull request as draft July 1, 2026 17:00
@henri318
henri318 marked this pull request as ready for review July 3, 2026 11:20
Developer added 2 commits July 3, 2026 13:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
modules/products/application/update-product-use-case.ts (2)

80-97: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Bug de integridad: se usa translations[0] en vez de la traducción del locale objetivo.

currentTranslation se toma como product.translations[0], no como la traducción existente para dto.locale. Si se actualiza un producto en un locale distinto al de la primera traducción (p. ej. dto.locale = 'cat' con translations[0] en 'es') sin enviar name/description, nextTranslation heredará el name/description de la traducción 'es', y al existir ya una traducción para 'cat' (translations.some(...)true), esta se sobrescribe con los valores de 'es'. Esto corrompe silenciosamente la traducción del locale correcto en cualquier actualización parcial (p. ej. solo cambiar price o status).

🐛 Corrección propuesta
-    const currentTranslation = product.translations[0];
+    const currentTranslation = product.translations.find(
+      (translation) => translation.locale === dto.locale,
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/application/update-product-use-case.ts` around lines 80 -
97, The locale-specific translation update in update-product-use-case.ts is
using the first translation as the fallback source instead of the existing
translation for dto.locale. Update the currentTranslation lookup in the product
update flow to select the translation whose locale matches dto.locale, then
build nextTranslation from that locale’s existing name/description. Keep the
existing translations.some/map replacement logic, but ensure it only overwrites
the matching locale with values derived from the same locale, not
product.translations[0].

123-134: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Unifica la actualización y el evento en una sola transacción. productRepository.update() abre su propia transacción y saveEvent() usa prisma por defecto, así que aquí se ejecutan por separado; si falla el outbox, el producto ya queda confirmado sin PRODUCT_UPDATED. Pasa ambos cambios por el mismo tx o encapsula esta lógica en un transactionRunner.run(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/application/update-product-use-case.ts` around lines 123 -
134, The update flow in updateProductUseCase currently persists the product and
writes the PRODUCT_UPDATED outbox event in separate transactions, so they can
get out of sync. Refactor the logic around ProductRepository.update and
OutboxRepository.saveEvent to run through the same transaction context, either
by passing the same tx to both calls or by wrapping both operations inside
transactionRunner.run(...), so the product update and event emission commit or
roll back together.
app/[locale]/seller/products/product-form.tsx (1)

321-345: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Los errores de validación en campos distintos a name/description/price/customizationConfig no se muestran.

FormErrors solo define claves para name, description, price y customizationConfig. Si productFormSchema.safeParse devuelve un issue con path[0] === 'images' (p. ej. lista vacía o alt inválido no bloqueado por el required nativo), mapErrors lo guardaría igualmente en el objeto (JS no aplica el tipo en runtime), pero no hay ningún errors.images renderizado en el JSX, así que el vendedor no vería ningún mensaje y el formulario simplemente no se enviaría sin explicación aparente.

🐛 Propuesta de corrección
 interface FormErrors {
   name?: string;
   description?: string;
   price?: string;
   customizationConfig?: string;
+  images?: string;
 }

Y renderizar errors.images cerca de ProductPhotoGallery, por ejemplo pasándolo como prop de error adicional o mostrándolo junto al photoError existente.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/product-form.tsx around lines 321 - 345, The
validation mapping in handleSubmit/mapErrors captures issues for fields like
images, but ProductForm only renders errors for name, description, price, and
customizationConfig, so image-related schema failures are hidden. Update
ProductForm to surface errors.images in the JSX near ProductPhotoGallery, either
by passing it as an additional prop or combining it with the existing photoError
display, and keep mapErrors aligned with the fields that can be reported by
productFormSchema.safeParse.
app/api/cart/route.ts (1)

36-41: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Regresión de rendimiento: resolución secuencial de productos.

El poblamiento de productMap pasó de resolver productIds en paralelo (Promise.all) a hacerlo secuencialmente con await dentro del for...of. Esto convierte N consultas potencialmente paralelas en N consultas en cadena, aumentando la latencia de GET /api/cart de forma lineal con la cantidad de productos distintos en el carrito.

⚡ Fix propuesto: restaurar resolución en paralelo
-    const productMap = new Map<string, ProductEntity>();
-    for (const id of productIds) {
-      const product = await productRepository.findById(id, 'es');
-      if (product) productMap.set(product.id, product);
-    }
+    const productMap = new Map<string, ProductEntity>();
+    const products = await Promise.all(
+      productIds.map((id) => productRepository.findById(id, 'es')),
+    );
+    for (const product of products) {
+      if (product) productMap.set(product.id, product);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/cart/route.ts` around lines 36 - 41, The `GET /api/cart` product
loading path is now resolving `productIds` one by one inside the `for...of`,
which makes `productRepository.findById` run sequentially and hurts latency.
Restore parallel fetching in the `productMap` population flow by collecting the
per-id lookups first and awaiting them together, then fill `productMap` from the
resolved results. Use the existing `productIds`, `productMap`, and
`productRepository.findById` logic in `app/api/cart/route.ts` to keep the change
localized.
modules/cart/application/update-cart-item.ts (1)

81-88: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Validar customizationIdList antes de persistirlo

En modules/cart/application/update-cart-item.ts:81-88, se guarda dto.customizationIdList tal cual. Falta comprobar que esos IDs existan y pertenezcan al productId del ítem, como ya hace add-item-to-cart.ts; si no, un usuario puede asociar personalizaciones ajenas y la ruta de lectura las resolverá y devolverá en customizations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/application/update-cart-item.ts` around lines 81 - 88, The
update flow in updateCartItem currently persists dto.customizationIdList without
validating it, unlike addItemToCart. Add the same validation before building
updatedItem so every customization ID exists and belongs to the item’s
productId, and reject invalid lists before save. Use the existing updateCartItem
logic and any shared validation helpers or patterns from add-item-to-cart.ts to
keep the behavior consistent.
♻️ Duplicate comments (1)
modules/presentation/components/design-preview.tsx (1)

37-61: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mismo problema de estado del contexto 2D que en app/[locale]/cart/design-preview.tsx.

draw() no reinicia la matriz de transformación ni globalAlpha/globalCompositeOperation, por lo que los repintados posteriores acumulan el estado del render anterior. Aplica el mismo reinicio (setTransform + save/restore) aquí.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/presentation/components/design-preview.tsx` around lines 37 - 61, The
2D canvas context in draw() is carrying over transform and compositing state
between repaints. In design-preview.tsx, update the draw() flow to reset the
context before drawing by using the canvas context’s transform reset and a
save/restore pattern around any changes to globalAlpha and
globalCompositeOperation, so repeated renders do not accumulate state. Keep the
fix localized to draw() and the existing drawCover/drawDesign calls.
🧹 Nitpick comments (23)
app/[locale]/products/[id]/customization-form.tsx (1)

102-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Selección de color sin semántica de "radio group" para lectores de pantalla.

Cada opción usa role="button" individual; para un selector de una sola opción entre varias, role="radiogroup"/role="radio" con aria-checked comunicaría mejor el estado seleccionado.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-form.tsx around lines 102 - 114,
The color picker in customization-form.tsx uses individual clickable divs with
role="button", which does not expose a single-choice selection pattern to
assistive tech. Update the component that renders the image options so the
container uses a radiogroup pattern and each option uses role="radio" with
aria-checked tied to the selected state, while keeping the existing
click/keyboard selection behavior in the same rendering logic.
components/cart/customization-draft-schema.ts (1)

3-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistencia de nomenclatura: rotation_deg/blend_mode en snake_case.

El resto de campos del payload usa camelCase (imageUrl, imageUploadId). Si no hay una razón de contrato externo (p. ej. mapear 1:1 con una librería canvas), sería más consistente usar rotationDeg/blendMode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/cart/customization-draft-schema.ts` around lines 3 - 20, The
schema in designPositionSchema is inconsistent with the rest of the payload by
using snake_case fields rotation_deg and blend_mode. Update the field names to
camelCase, matching imageUrl and other cart customization fields, and adjust any
related parsing/usage sites that reference designPositionSchema or
designBlendModeSchema so they read and write rotationDeg and blendMode
consistently.
components/cart/add-to-cart-button.tsx (1)

264-264: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Recalcula el borrador normalizado en vez de reutilizar normalizedCustomization.

handleSaveDesign llama a normalizeCustomizationDraft(customization) de nuevo, ignorando el valor ya memoizado normalizedCustomization calculado con las mismas entradas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/cart/add-to-cart-button.tsx` at line 264, The design-save flow
recomputes the normalized customization draft instead of reusing the already
memoized value. Update handleSaveDesign in add-to-cart-button.tsx to use
normalizedCustomization rather than calling
normalizeCustomizationDraft(customization) again, and keep the existing
normalization logic centralized around normalizeCustomizationDraft and
normalizedCustomization.
tests/unit/components/cart/add-to-cart-button.test.tsx (1)

264-312: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Falta cobertura de test para designPosition en el matching de personalización.

Estos tests validan campos de texto/color/tamaño/imagen pero no designPosition. Dado el hallazgo en add-to-cart-button.tsx donde guestCustomizationMatches/authCustomizationMatches ignoran designPosition, un test que reproduzca dos borradores con igual texto/color/tamaño pero distinta posición ayudaría a exponer esta laguna.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/components/cart/add-to-cart-button.test.tsx` around lines 264 -
312, Add a test in AddToCartButton coverage that exercises designPosition in
customization matching, since the current guest cart test only checks
text/color/size/image fields. In the add-to-cart-button.test.tsx suite, extend
the existing guest customization scenario or add a new case that creates two
customizations with identical text, color, size, and image values but different
designPosition values, then assert that the matching logic in AddToCartButton
distinguishes them and sends the correct payload through mockAddItem. Use the
AddToCartButton component and the guest customization setup to locate and
validate the guestCustomizationMatches/authCustomizationMatches behavior.
app/api/customizations/route.ts (1)

78-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lógica de mapeo de designPosition duplicada y tipado unknown innecesario.

El casteo de designPosition de unknown a la forma específica se repite de forma idéntica en app/api/customizations/[id]/route.ts. Además, el parámetro se tipa como unknown cuando la entidad de dominio (CustomizationEntity.designPosition: DesignPositionValue | null) ya aporta el tipo correcto, por lo que el as {...} es un casteo innecesario que oculta información de tipos ya disponible.

Sugiero extraer un serializador compartido dentro del módulo customizations (p. ej. en modules/customizations/presentation/) que reciba CustomizationEntity directamente, eliminando la duplicación y el cast inseguro.

♻️ Refactor propuesto
-function toCustomizationResponse(customization: {
-  id: string;
-  productId: string;
-  text: string | null;
-  color: string | null;
-  size: string | null;
-  imageUrl: string | null;
-  designPosition: unknown;
-  createdAt: Date;
-}) {
-  return customizationResponseSchema.parse({
-    id: customization.id,
-    productId: customization.productId,
-    text: customization.text,
-    color: customization.color,
-    size: customization.size,
-    imageUrl: customization.imageUrl,
-    designPosition:
-      (customization.designPosition as
-        | {
-            imageUrl: string;
-            x: number;
-            y: number;
-            scale: number;
-            rotation_deg: number;
-            opacity: number;
-            blend_mode: string;
-          }
-        | null
-        | undefined) ?? null,
-    createdAt: customization.createdAt.toISOString(),
-  });
-}
+// Importar el serializador compartido en vez de redefinirlo:
+import { toCustomizationResponse } from '`@/modules/customizations/presentation/serializers/customization-response`';
// modules/customizations/presentation/serializers/customization-response.ts
import type { CustomizationEntity } from '`@/modules/customizations/domain/entities/customization`';
import { customizationResponseSchema } from '`@/modules/customizations/presentation/schemas/customization-schemas`';

export function toCustomizationResponse(customization: CustomizationEntity) {
  return customizationResponseSchema.parse({
    ...customization,
    designPosition: customization.designPosition ?? null,
    createdAt: customization.createdAt.toISOString(),
  });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/customizations/route.ts` around lines 78 - 110, The
`toCustomizationResponse` mapping repeats the same `designPosition` cast logic
and uses `unknown` even though `CustomizationEntity` already provides the
correct type. Extract a shared serializer (for example `toCustomizationResponse`
in the customizations presentation layer) that accepts `CustomizationEntity`
directly, maps `designPosition` with a simple null fallback, and reuses it from
both customization route handlers to remove duplication and the unsafe cast.
app/[locale]/products/[id]/customization-experience.module.css (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Corregir hallazgos de Stylelint en el archivo nuevo.

El linter marca varios problemas reales en este CSS recién añadido: notación de color/alfa obsoleta (rgba(...)rgb(... / %)), selectores no kebab-case (.canvasCol, .formCol) y notación de rango de media feature obsoleta.

🎨 Fix propuesto
-.canvasCol {
+.canvas-col {
   display: grid;
   gap: 1rem;
 }
 
-.formCol {
+.form-col {
   display: grid;
   gap: 1rem;
 }
   background: rgba(255, 255, 255, 0.92);
   border: 1px solid rgba(13, 92, 70, 0.1);
   box-shadow: 0 12px 28px rgba(13, 92, 70, 0.08);
+  /* stylelint: usar notación moderna */
+  background: rgb(255 255 255 / 92%);
+  border: 1px solid rgb(13 92 70 / 10%);
+  box-shadow: 0 12px 28px rgb(13 92 70 / 8%);
-@media (max-width: 768px) {
+@media (width <= 768px) {

Recuerda actualizar las referencias styles.canvasCol / styles.formCol en el componente TSX correspondiente si renombras las clases.

Also applies to: 23-23, 35-37, 48-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-experience.module.css at line 18,
Stylelint is flagging several issues in the new customization-experience
stylesheet: obsolete alpha color notation, non-kebab-case class selectors, and
an outdated media feature range syntax. Update the CSS in
customization-experience.module.css by renaming the selectors used in the module
(such as canvasCol and formCol) to kebab-case and then adjust the corresponding
styles.canvasCol and styles.formCol references in the related TSX component.
Also replace any rgba-based alpha usage with the modern rgb(... / ...) syntax
and convert the media query range to the current recommended form, keeping the
same visual behavior.

Source: Linters/SAST tools

modules/products/presentation/schemas/product-form-schema.ts (1)

13-53: 📐 Maintainability & Code Quality | 🔵 Trivial

Duplicación de constantes de validación entre capa de presentación y dominio.

designPositionInputSchema reproduce con números literales (0-1, 10-200, -45/45, 0-100) los mismos límites que ya están centralizados como MIN_DESIGN_SCALE_PERCENT/MAX_DESIGN_SCALE_PERCENT/etc. en product-customization-config.ts (capa de dominio). Además, productCustomizationConfigSchema aquí impone .max(2000) en designChangeDescription y .max(20) en tagNames, límites que el esquema equivalente del dominio no reproduce, dejando una vía de entrada sin ese tope si algo invoca ProductCustomizationConfig.fromJson() directamente.

Considera importar las constantes/el esquema del dominio en lugar de duplicarlos, para evitar que ambos se desincronicen.

Basado en el aprendizaje recuperado: "Keep business logic in the domain layer (Domain-First)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/presentation/schemas/product-form-schema.ts` around lines 13
- 53, El esquema de presentación está duplicando reglas de negocio que ya viven
en el dominio y además añade límites extra que no existen allí. Ajusta
`designPositionInputSchema` y `productCustomizationConfigSchema` para reutilizar
las constantes/esquemas centralizados desde `product-customization-config.ts`
(por ejemplo en `ProductCustomizationConfig.fromJson()` y los valores de
`MIN_*/MAX_*`), en lugar de repetir literales como `0-1`, `10-200`, `-45/45`,
`0-100`, `2000` y `20`. Mantén la validación de presentación alineada con el
dominio para evitar desincronización entre capas.

Source: Learnings

tmp/check-db.ts (1)

1-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Evaluar si este script de depuración debe versionarse.

tmp/check-db.ts consulta directamente todas las tablas (bypass de repositorios/módulos) y no tiene ninguna guarda de entorno (p. ej. bloquear ejecución si NODE_ENV=production). Considera moverlo a .gitignore o añadir una comprobación explícita de entorno antes de ejecutarlo contra una base de datos real.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tmp/check-db.ts` around lines 1 - 49, The debug script in check-db.ts should
not be treated as a normal versioned runtime path because it queries the
database directly without any environment safety check. Either move this helper
out of the tracked code path (or ensure it is ignored as a local-only script) or
add an explicit guard in main()/startup that refuses to run when NODE_ENV
indicates production, so the PrismaClient/PrismaPg queries only execute in safe
environments.
app/[locale]/products/[id]/mockup-canvas-control.module.css (1)

122-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicación de .fileInput y .visuallyHidden, y uso de la propiedad deprecada clip.

Ambas clases implementan el mismo patrón "oculto visualmente pero accesible" casi idéntico, y usan clip: rect(0, 0, 0, 0), marcado como deprecado por stylelint (property-no-deprecated). Conviene consolidar en una única clase reutilizable y usar el reemplazo moderno clip-path: inset(50%).

♻️ Propuesta de consolidación
-.fileInput {
-  position: absolute;
-  width: 1px;
-  height: 1px;
-  margin: -1px;
-  padding: 0;
-  overflow: hidden;
-  clip: rect(0, 0, 0, 0);
-  border: 0;
-}
+.fileInput,
+.visuallyHidden {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip-path: inset(50%);
+  white-space: nowrap;
+  border: 0;
+}
-.visuallyHidden {
-  position: absolute;
-  width: 1px;
-  height: 1px;
-  padding: 0;
-  margin: -1px;
-  overflow: hidden;
-  clip: rect(0, 0, 0, 0);
-  white-space: nowrap;
-  border: 0;
-}

Also applies to: 191-201

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/mockup-canvas-control.module.css around lines 122
- 131, The `.fileInput` and `.visuallyHidden` styles duplicate the same
visually-hidden pattern, and `clip` is deprecated. Consolidate the shared
accessibility utility into a single reusable class in
`mockup-canvas-control.module.css`, then update the hidden-input styling to use
the modern `clip-path: inset(50%)` approach instead of `clip: rect(...)`. Make
sure any references to `.fileInput` or `.visuallyHidden` are updated to use the
unified class consistently.

Source: Linters/SAST tools

app/[locale]/seller/products/product-form.tsx (1)

65-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

productId es opcional incluso en modo 'edit', sin garantía de tipo.

Si en el futuro se omite productId con mode="edit", endpoint resultaría en /api/products/undefined sin error de compilación. Una unión discriminada por mode evitaría este caso en tiempo de compilación.

♻️ Propuesta
-interface ProductFormProps {
-  locale: string;
-  mode: ProductFormMode;
-  productId?: string;
-  initialValues: {
+type ProductFormProps = {
+  locale: string;
+  initialValues: {
     name: string;
     description: string;
     price: number;
     customizationConfig: ProductCustomizationConfigInput;
     images: Array<{
       url: string;
       alt: string | null;
     }>;
   };
   labels: ProductFormLabels;
   categories?: CategoryOption[];
-}
+} & ({ mode: 'create' } | { mode: 'edit'; productId: string });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/product-form.tsx around lines 65 - 81, The
ProductFormProps shape allows productId to be missing even when mode is edit,
which can lead to an undefined endpoint in ProductForm. Refactor the props into
a discriminated union keyed by mode so that ProductFormMode.edit requires
productId and the non-edit variant does not, then update the ProductForm
component’s endpoint construction to rely on that typed guarantee.
modules/presentation/components/cart-popup.module.css (1)

124-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Añadir línea en blanco entre reglas para cumplir rule-empty-line-before.

Stylelint señala falta de línea en blanco antes de cada uno de los 4 nuevos bloques (.popupItemDetails, .popupCustLine, .popupCustText, .popupDesignThumb).

🎨 Fix propuesto
 .seller {
   font-size: 0.72rem;
   color: var(--color-text-secondary, `#666`);
   display: block;
   margin-top: 2px;
 }
+
 .popupItemDetails {
   display: flex;
   flex-direction: column;
   gap: 1px;
   min-width: 0;
 }
+
 .popupCustLine {
   font-size: 0.72rem;
   color: `#555`;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+
 .popupCustText {
   font-size: 0.7rem;
   color: `#777`;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+
 .popupDesignThumb {
   width: 28px;
   height: 28px;
   border-radius: 4px;
   object-fit: cover;
   flex-shrink: 0;
   margin-top: 2px;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/presentation/components/cart-popup.module.css` around lines 124 -
151, Stylelint is flagging missing blank lines between the new CSS rules in
cart-popup.module.css; update the stylesheet so each of the four selectors,
.popupItemDetails, .popupCustLine, .popupCustText, and .popupDesignThumb, has an
empty line before the next rule to satisfy rule-empty-line-before and keep the
block formatting consistent.

Source: Linters/SAST tools

shared/presentation/components/file-upload-dropzone.module.css (2)

91-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Propiedad clip deprecada; usar clip-path.

clip está deprecada por CSS Masking Level 1. Para el patrón "visually hidden" del input, la alternativa moderna y ampliamente soportada es clip-path: inset(50%).

♿ Fix propuesto
 .input {
   position: absolute;
   width: 1px;
   height: 1px;
   margin: -1px;
   padding: 0;
   overflow: hidden;
   border: 0;
-  clip: rect(0, 0, 0, 0);
+  clip-path: inset(50%);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/presentation/components/file-upload-dropzone.module.css` around lines
91 - 100, The visually-hidden styling for the input currently uses the
deprecated clip property, so update the .input rule in
file-upload-dropzone.module.css to use the modern clip-path approach instead.
Keep the existing hidden-accessible pattern intact in the same .input selector
by replacing clip with clip-path: inset(50%) and preserving the other
positioning/overflow styles.

Source: Linters/SAST tools


4-153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Notación de color moderna (rgba()rgb() con porcentaje) señalada por Stylelint.

Múltiples reglas (líneas 4, 8, 9, 15, 21, 62, 69, 105, 120, 121, 125, 126, 152, 153) usan rgba(r, g, b, alpha-decimal), marcado por color-function-notation/alpha-value-notation/color-function-alias-notation. Es puramente estilístico (no cambia el color renderizado), pero al ser un archivo nuevo conviene alinearlo con la configuración de Stylelint del proyecto para evitar fallos de lint en CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/presentation/components/file-upload-dropzone.module.css` around lines
4 - 153, The CSS in file-upload-dropzone.module.css uses legacy rgba() color
notation in multiple rules, which is triggering Stylelint’s color-function
checks. Update the affected color declarations in the dropzone styles (including
the root, focus, header text, trigger, busy/empty, item, selected, size, and
remove button rules) to the project-preferred modern rgb() percentage syntax
with alpha. Keep the same visual values, and make sure every unique color token
in this stylesheet follows the same notation consistently so the new file passes
lint.

Source: Linters/SAST tools

modules/cart/infrastructure/customization-lookup-adapter.ts (1)

52-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extraer la coerción de designPosition a un helper compartido.
Esta lógica es idéntica a modules/orders/infrastructure/prisma-order-repository.ts; moverla a un helper en shared/ evitaría divergencias futuras.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/infrastructure/customization-lookup-adapter.ts` around lines 52
- 76, The `toDesignPositionSnapshot` coercion logic is duplicated and should be
centralized in a shared helper to prevent drift between
`CustomizationLookupAdapter` and the matching mapping in
`prisma-order-repository`. Extract the snapshot normalization into a reusable
helper under `shared/`, then update both call sites to use that helper while
keeping the same `CustomizatonDesignPositionSnapshot` shape and fallback
defaults.
app/[locale]/products/[id]/customization-draft-context.tsx (1)

123-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

setDesignPosition no limpia error/errors como el resto de setters.

A diferencia de setText, setColor, setSize, setImage y clearImage, este setter no resetea draft.error ni llama a clearValidationErrors(), dejando potencialmente un mensaje de error obsoleto visible tras ajustar la posición del diseño.

♻️ Ajuste propuesto para consistencia
   const setDesignPosition = useCallback((value: DesignPosition | null) => {
     setDraft((current) => {
       if (!value) {
-        return { ...current, designPosition: null };
+        return { ...current, designPosition: null, error: null };
       }
-      return { ...current, designPosition: value };
+      return { ...current, designPosition: value, error: null };
     });
-  }, []);
+    clearValidationErrors();
+  }, [clearValidationErrors]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-draft-context.tsx around lines 123
- 130, `setDesignPosition` is inconsistent with the other draft setters because
it updates `designPosition` without clearing stale validation state. Update the
`setDesignPosition` callback in `CustomizationDraftContext` to match `setText`,
`setColor`, `setSize`, `setImage`, and `clearImage` by resetting `draft.error`
and invoking `clearValidationErrors()` whenever the design position changes,
including when it is set to null.
modules/presentation/components/cart-popup.tsx (1)

114-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Casting manual extenso de la respuesta del carrito.

El mapeo con múltiples as (i.id as string, etc.) elude la verificación de tipos y es frágil si la forma de la respuesta cambia. Considera validar/parsear la respuesta (p. ej. un esquema Zod) para obtener un DTO tipado de forma segura.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/presentation/components/cart-popup.tsx` around lines 114 - 145, The
cart item mapping in cart-popup.tsx is relying on many unsafe `as` casts inside
the items map, which bypasses type safety and is fragile. Replace this manual
coercion with proper response validation/parsing before building the
CartItemDTO, ideally using a schema such as Zod (or a dedicated parser/helper
near the existing cart data handling) so fields like id, productId, sellerId,
quantity, unitPrice, and customization are validated instead of blindly cast.
app/[locale]/cart/design-preview.tsx (1)

1-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Archivo duplicado.

Este componente es idéntico a modules/presentation/components/design-preview.tsx. Considera extraerlo a una única ubicación compartida y reutilizarlo para evitar que las correcciones (como el reinicio del contexto) se apliquen solo en una de las copias.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/cart/design-preview.tsx around lines 1 - 140, This
`DesignPreview` component is duplicated and needs to be centralized so fixes
apply in one place only. Move the shared implementation of `DesignPreview`,
`loadImage`, `drawCover`, and `drawDesign` into a single reusable
component/module, then have both copies import it instead of maintaining
separate files. Keep the existing `DesignPreview` API intact while replacing the
duplicate local implementation with the shared one.
modules/cart/presentation/guest-cart-context.tsx (1)

178-285: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lógica de personalización duplicada.

updateCustomization y updateItemCustomization son prácticamente idénticas; solo difieren en la clave de coincidencia (productId vs id). Extrae la fusión de campos de personalización a un helper compartido para evitar que ambas ramas diverjan al añadir nuevos campos.

♻️ Ejemplo de helper compartido
+function mergeCustomization(
+  item: GuestCartItem,
+  customization: {
+    text?: string | null;
+    color?: string | null;
+    size?: string | null;
+    imageUrl?: string | null;
+    imageUploadId?: string | null;
+    designPosition?: Record<string, unknown> | null;
+  },
+): GuestCartItem {
+  return {
+    ...item,
+    customizationText:
+      customization.text !== undefined ? customization.text : item.customizationText,
+    customizationColor:
+      customization.color !== undefined ? customization.color : item.customizationColor,
+    customizationSize:
+      customization.size !== undefined ? customization.size : item.customizationSize,
+    customizationImageUrl:
+      customization.imageUrl !== undefined ? customization.imageUrl : item.customizationImageUrl,
+    customizationImageUploadId:
+      customization.imageUploadId !== undefined
+        ? customization.imageUploadId
+        : item.customizationImageUploadId,
+    customizationDesignPosition:
+      customization.designPosition !== undefined
+        ? (customization.designPosition as GuestCartItem['customizationDesignPosition'])
+        : item.customizationDesignPosition,
+  };
+}

Luego cada callback queda como prev.map((item) => item.productId === productId ? mergeCustomization(item, customization) : item) (y análogo por id).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/guest-cart-context.tsx` around lines 178 - 285,
Both updateCustomization and updateItemCustomization duplicate the same
customization merge logic, so extract that field-merging into a shared helper in
guest-cart-context.tsx and reuse it from both callbacks. Keep the helper
responsible for applying text, color, size, imageUrl, imageUploadId, and
designPosition onto a cart item, while each callback only handles its own match
condition (productId vs id). This will keep the behavior identical and prevent
the two branches from drifting when new customization fields are added.
modules/sellers/infrastructure/prisma-seller-repository.ts (1)

87-93: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Ejecuta findMany y count en paralelo.

Al sustituir la transacción combinada por dos await secuenciales se añade una ida y vuelta adicional a la base de datos. Como no dependen entre sí, pueden lanzarse en paralelo con Promise.all para reducir la latencia.

♻️ Refactor propuesto
-    const rows = await prisma.seller.findMany({
-      where,
-      orderBy: { [sortBy]: sortDir },
-      skip: (page - 1) * pageSize,
-      take: pageSize,
-    });
-    const total = await prisma.seller.count({ where });
+    const [rows, total] = await Promise.all([
+      prisma.seller.findMany({
+        where,
+        orderBy: { [sortBy]: sortDir },
+        skip: (page - 1) * pageSize,
+        take: pageSize,
+      }),
+      prisma.seller.count({ where }),
+    ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/sellers/infrastructure/prisma-seller-repository.ts` around lines 87 -
93, The seller repository query is running `prisma.seller.findMany` and
`prisma.seller.count` sequentially, adding unnecessary latency. Update the
`prisma-seller-repository.ts` logic around the seller listing flow to launch
both operations in parallel with `Promise.all`, then destructure the results for
`rows` and `total` while keeping the existing `where`, `sortBy`, `sortDir`,
`skip`, and `take` behavior unchanged.
.atl/skill-registry.md (1)

9-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rutas absolutas específicas de una máquina personal.

El registro referencia rutas Windows ligadas a un usuario concreto (C:\Users\Mi Pc\...). Cualquier otro colaborador o agente que ejecute este flujo en otra máquina no podrá resolver los SKILL.md indicados, rompiendo el "Loading protocol" descrito más abajo.

Considera generar este archivo con rutas relativas al repositorio o basadas en variables de entorno, o excluirlo del control de versiones si es un artefacto puramente local del entorno del desarrollador.

Also applies to: 26-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.atl/skill-registry.md around lines 9 - 14, El registro en
.atl/skill-registry.md usa rutas absolutas de Windows ligadas a un
usuario/máquina concreta, así que debes reemplazarlas por rutas relativas al
repositorio o referencias configurables mediante variables de entorno para que
el loading de SKILL.md funcione en cualquier entorno; ajusta la lista en el
contenido de skill-registry para que no dependa de C:\Users\Mi Pc ni de nombres
locales.
composition-root/container.ts (1)

244-249: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comentario desactualizado tras el cambio.

El comentario // --- StoragePort: R2 adapter for uploads --- en la línea 244 ya no refleja el comportamiento real (ahora también usa LocalStorageAdapter fuera de producción).

✏️ Ajuste sugerido
-  // --- StoragePort: R2 adapter for uploads ---
+  // --- StoragePort: R2 adapter in production, local filesystem adapter otherwise ---
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition-root/container.ts` around lines 244 - 249, The StoragePort
comment is stale because the factory in container.ts now selects either
R2StorageAdapter or LocalStorageAdapter based on NODE_ENV. Update the comment
near the _storagePort initialization to describe the actual environment-based
adapter selection, keeping it aligned with the logic in the storage port setup
block.
modules/cart/presentation/schemas/cart-schemas.ts (1)

48-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mezcla de convenciones de nombres (rotation_deg, blend_mode vs. camelCase).

El resto del esquema (imageUrl, customizationImageUploadId, etc.) usa camelCase, pero rotation_deg y blend_mode están en snake_case. Si no es un contrato externo (p. ej. formato de un editor de imágenes de terceros) que exige snake_case, conviene alinear a camelCase por consistencia.

Además, .strict().nullable().optional() puede simplificarse con .nullish().

♻️ Sugerencia (si no hay contrato externo con snake_case)
 const designPositionMigrationSchema = z
   .object({
     imageUrl: z.string().min(1),
     x: z.number().min(0).max(1),
     y: z.number().min(0).max(1),
     scale: z.number().min(10).max(200),
-    rotation_deg: z.number().min(-45).max(45),
+    rotationDeg: z.number().min(-45).max(45),
     opacity: z.number().min(0).max(100),
-    blend_mode: z.enum(['source-over', 'multiply', 'overlay', 'soft-light']),
+    blendMode: z.enum(['source-over', 'multiply', 'overlay', 'soft-light']),
   })
   .strict()
-  .nullable()
-  .optional();
+  .nullish();

Si rotation_deg/blend_mode reflejan un contrato de persistencia o de un componente de canvas externo, este cambio podría romper compatibilidad; conviene confirmarlo antes de renombrar.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/schemas/cart-schemas.ts` around lines 48 - 60, The
designPositionMigrationSchema still mixes snake_case and camelCase, so update
the field names in the schema to match the rest of cart-schemas.ts unless they
must stay aligned with an external contract; specifically review rotation_deg
and blend_mode in designPositionMigrationSchema and rename them consistently if
safe. Also simplify the nullable/optional chaining by replacing
strict().nullable().optional() with nullish() on that same schema to keep the
definition cleaner and equivalent.
modules/cart/application/update-cart-item.ts (1)

100-105: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

El evento CART_ITEM_UPDATED no incluye customizationIdList.

Ahora el ítem puede actualizar su personalización, pero el payload del evento solo reporta quantity. Los consumidores del bus de eventos (arquitectura event-driven) no se enterarán de este cambio.

♻️ Ajuste sugerido
     await this.outboxRepository.saveEvent(GlobalEvents.CART_ITEM_UPDATED, {
       cartId: cart.id,
       itemId: updatedItem.id,
       quantity: updatedItem.quantity,
+      customizationIdList: updatedItem.customizationIdList,
       occurredAt: new Date().toISOString(),
     });

Basado en el aprendizaje recuperado: "All cross-module communication must use the internal event bus (Event-Driven)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/application/update-cart-item.ts` around lines 100 - 105, El
payload de `GlobalEvents.CART_ITEM_UPDATED` en `update-cart-item.ts` no está
propagando `customizationIdList`, así que los consumidores no reciben ese
cambio. Ajusta la llamada a `this.outboxRepository.saveEvent` dentro de
`UpdateCartItem` para incluir también `updatedItem.customizationIdList` junto
con `cartId`, `itemId`, `quantity` y `occurredAt`, manteniendo el evento
alineado con el estado real del ítem.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: afe044ea-f7b3-49ac-b8af-7adb3ff1d243

📥 Commits

Reviewing files that changed from the base of the PR and between 2e20299 and c26333c.

⛔ Files ignored due to path filters (23)
  • devresources/products/azul.png is excluded by !**/*.png
  • devresources/products/paisaje-profundidad-lineas-734x489.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0424fd7c-41cf-48ee-8215-2a1f7636c1f9.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0ba87ba4-8a04-478b-88a2-1ecc490dabbc.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/54b3f06b-2044-456a-9757-75505771f677.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/7072116f-5974-42f6-8344-23861596b58e.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/86912d37-6fc1-4f24-8ff5-fae00ff1eb12.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ade99071-3c02-4ed4-b640-f96aa3dd2db4.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ddac64c9-c822-4631-93f4-e1f1133caa04.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/0770179d-365d-4b8a-b278-453d1e6b717e.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/51d82e86-9260-4eab-9fac-77513ad454cd.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/72e1a4df-88ce-4d82-ae8e-300a65f8c76d.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/7748b1c2-6b6d-4dc2-b3d5-f8717a581d6c.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/859fae6e-2c78-46ee-b025-1fd8048e87b5.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ed250a15-8870-40f6-a1ec-8a8845ada3ed.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ef719e39-2897-4106-ad16-a2339739f06a.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/34162f3a-0a1e-4c92-8088-99db5e0cd3e9.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/c9abdded-5e1b-4697-b236-d3380155bb5c.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/cfe23339-3140-4855-99d0-cbf16d3b8412.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/530f7dac-9b5f-4a14-adc3-717a7f603532.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/9026017f-9c52-4e5b-b6be-2c8e8649caf6.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/94a8b1bb-3778-4cfa-80f2-38e2778cf1c2.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/a8dfbda4-a431-406d-835e-9bd18e9f7eb2.png is excluded by !**/*.png
📒 Files selected for processing (121)
  • .atl/.skill-registry.cache.json
  • .atl/skill-registry.md
  • app/[locale]/cart/cart-view.module.css
  • app/[locale]/cart/cart-view.tsx
  • app/[locale]/cart/design-preview.tsx
  • app/[locale]/cart/page.tsx
  • app/[locale]/checkout/page.tsx
  • app/[locale]/page.module.css
  • app/[locale]/page.tsx
  • app/[locale]/products/[id]/customization-draft-context.tsx
  • app/[locale]/products/[id]/customization-experience.module.css
  • app/[locale]/products/[id]/customization-experience.tsx
  • app/[locale]/products/[id]/customization-form.module.css
  • app/[locale]/products/[id]/customization-form.tsx
  • app/[locale]/products/[id]/customization-preview.tsx
  • app/[locale]/products/[id]/mockup-canvas-control.module.css
  • app/[locale]/products/[id]/mockup-canvas-control.tsx
  • app/[locale]/products/[id]/page.module.css
  • app/[locale]/products/[id]/page.tsx
  • app/[locale]/products/[id]/photo-upload-field.tsx
  • app/[locale]/products/[id]/preview-overlay.module.css
  • app/[locale]/seller/products/[id]/edit/page.tsx
  • app/[locale]/seller/products/new/page.tsx
  • app/[locale]/seller/products/page.module.css
  • app/[locale]/seller/products/page.tsx
  • app/[locale]/seller/products/product-form.module.css
  • app/[locale]/seller/products/product-form.tsx
  • app/[locale]/seller/products/product-photo-gallery.tsx
  • app/api/cart/items/[itemId]/route.ts
  • app/api/cart/items/route.ts
  • app/api/cart/migrate/route.ts
  • app/api/cart/route.ts
  • app/api/customizations/[id]/route.ts
  • app/api/customizations/customer/route.ts
  • app/api/customizations/route.ts
  • app/api/products/[id]/route.ts
  • app/api/products/route.ts
  • app/api/uploads/local/[...storageKey]/route.ts
  • components/cart/add-to-cart-button.module.css
  • components/cart/add-to-cart-button.tsx
  • components/cart/add-to-cart-choice-modal.module.css
  • components/cart/add-to-cart-choice-modal.tsx
  • components/cart/customization-draft-schema.ts
  • composition-root/container.ts
  • devresources/products/example.webp
  • modules/cart/application/migrate-guest-cart.ts
  • modules/cart/application/update-cart-item.ts
  • modules/cart/domain/customization-create-port.ts
  • modules/cart/domain/customization-lookup-port.ts
  • modules/cart/infrastructure/customization-lookup-adapter.ts
  • modules/cart/presentation/guest-cart-context.tsx
  • modules/cart/presentation/schemas/cart-schemas.ts
  • modules/customizations/application/__tests__/customization-queries.test.ts
  • modules/customizations/application/__tests__/customization-use-cases.test.ts
  • modules/customizations/application/create-customer-customization.ts
  • modules/customizations/application/create-customization.ts
  • modules/customizations/application/update-customization.ts
  • modules/customizations/domain/__tests__/customization-domain.test.ts
  • modules/customizations/domain/entities/customization.ts
  • modules/customizations/domain/value-objects/customization-options.ts
  • modules/customizations/infrastructure/prisma-customization-repository.ts
  • modules/customizations/presentation/schemas/customization-schemas.ts
  • modules/orders/application/handle-cart-checked-out.ts
  • modules/orders/domain/customization-lookup-port.ts
  • modules/orders/infrastructure/prisma-order-repository.ts
  • modules/presentation/components/cart-popup.module.css
  • modules/presentation/components/cart-popup.tsx
  • modules/presentation/components/design-preview.tsx
  • modules/products/application/create-product-use-case.ts
  • modules/products/application/update-product-use-case.ts
  • modules/products/domain/value-objects/product-customization-config.ts
  • modules/products/infrastructure/mapper.ts
  • modules/products/infrastructure/prisma-product-repository.ts
  • modules/products/presentation/components/product-customization-config-editor.module.css
  • modules/products/presentation/components/product-customization-config-editor.tsx
  • modules/products/presentation/schemas/product-form-schema.ts
  • modules/sellers/infrastructure/prisma-seller-repository.ts
  • modules/uploads/application/create-upload-use-case.ts
  • modules/uploads/infrastructure/local-storage-adapter.ts
  • next.config.ts
  • prisma/migrations/20260701100000_add_customization_design_position/migration.sql
  • prisma/schema.prisma
  • prisma/seed-data.ts
  • sdd/customizations-module/exploration.md
  • sdd/explore-shared-refactor/exploration.md
  • sdd/hexagonal-cleanup/tasks.md
  • sdd/seller-module/design.md
  • shared/i18n/locales/cat.json
  • shared/i18n/locales/es.json
  • shared/presentation/components/file-upload-dropzone.module.css
  • shared/presentation/components/file-upload-dropzone.tsx
  • shared/presentation/lib/to-absolute-url.ts
  • tests/doubles/memory-customization-lookup.ts
  • tests/doubles/memory-order-customization-lookup.ts
  • tests/integration/customizations/prisma-customization-repository.integration.test.ts
  • tests/integration/orders/handle-cart-checked-out.integration.test.ts
  • tests/integration/orders/prisma-order-repository.integration.test.ts
  • tests/unit/app/[locale]/page.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-form.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-preview.test.tsx
  • tests/unit/app/[locale]/products/[id]/page.test.tsx
  • tests/unit/app/[locale]/products/[id]/photo-upload-field-status.test.tsx
  • tests/unit/app/[locale]/products/[id]/photo-upload-field.test.tsx
  • tests/unit/app/[locale]/seller/products/product-form.test.tsx
  • tests/unit/app/api/uploads/local/[...storageKey]/route.test.ts
  • tests/unit/components/cart/add-to-cart-button.test.tsx
  • tests/unit/components/cart/guest-cart-badge.test.tsx
  • tests/unit/modules/cart/application/migrate-guest-cart.test.ts
  • tests/unit/modules/orders/application/handle-cart-checked-out.test.ts
  • tests/unit/modules/presentation/components/cart-icon.test.tsx
  • tests/unit/modules/products/application/create-product-use-case.test.ts
  • tests/unit/modules/products/application/update-product-use-case.test.ts
  • tests/unit/modules/products/domain/value-objects/product-customization-config.test.ts
  • tests/unit/modules/products/infrastructure/prisma-product-repository.test.ts
  • tests/unit/modules/products/infrastructure/product-customization-config-mapper.test.ts
  • tests/unit/modules/uploads/application/create-upload-use-case.test.ts
  • tests/unit/modules/uploads/infrastructure/local-storage-adapter.test.ts
  • tests/unit/shared/presentation/components/file-upload-dropzone.test.tsx
  • tmp/check-db.ts
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/41d48cda-1463-4903-a890-a75dc70860d2.webp
💤 Files with no reviewable changes (4)
  • sdd/customizations-module/exploration.md
  • sdd/seller-module/design.md
  • sdd/explore-shared-refactor/exploration.md
  • sdd/hexagonal-cleanup/tasks.md
✅ Files skipped from review due to trivial changes (8)
  • .atl/.skill-registry.cache.json
  • shared/presentation/lib/to-absolute-url.ts
  • prisma/migrations/20260701100000_add_customization_design_position/migration.sql
  • modules/customizations/domain/tests/customization-domain.test.ts
  • app/api/cart/items/route.ts
  • shared/i18n/locales/es.json
  • tests/unit/modules/products/application/update-product-use-case.test.ts
  • shared/i18n/locales/cat.json
🚧 Files skipped from review as they are similar to previous changes (24)
  • tests/unit/app/[locale]/products/[id]/photo-upload-field-status.test.tsx
  • modules/cart/domain/customization-create-port.ts
  • app/[locale]/seller/products/page.tsx
  • tests/unit/modules/products/domain/value-objects/product-customization-config.test.ts
  • tests/unit/modules/products/infrastructure/product-customization-config-mapper.test.ts
  • tests/unit/modules/products/application/create-product-use-case.test.ts
  • app/[locale]/checkout/page.tsx
  • app/[locale]/cart/page.tsx
  • modules/orders/application/handle-cart-checked-out.ts
  • app/api/products/route.ts
  • modules/products/application/create-product-use-case.ts
  • app/api/cart/migrate/route.ts
  • prisma/seed-data.ts
  • app/[locale]/products/[id]/customization-preview.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/app/[locale]/products/[id]/photo-upload-field.test.tsx
  • modules/customizations/application/create-customer-customization.ts
  • app/[locale]/products/[id]/photo-upload-field.tsx
  • app/api/customizations/customer/route.ts
  • app/api/products/[id]/route.ts
  • app/[locale]/cart/cart-view.tsx
  • tests/unit/modules/cart/application/migrate-guest-cart.test.ts
  • modules/products/infrastructure/mapper.ts
  • modules/cart/application/migrate-guest-cart.ts

Comment on lines +37 to +61
async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;

const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;

c.clearRect(0, 0, width, height);
c.fillStyle = '#f4f2e6';
c.fillRect(0, 0, width, height);

if (productImg) {
drawCover(c, productImg, width, height);
}

if (designImg) {
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El estado del contexto 2D no se reinicia entre repintados.

drawDesign aplica translate/rotate sobre la matriz del contexto, pero draw() nunca la restablece. Como el mismo <canvas>/contexto persiste entre renders, en el siguiente repintado clearRect/fillRect/drawCover se ejecutan bajo la matriz transformada del render anterior, y globalAlpha/globalCompositeOperation también quedan con valores residuales. Esto produce recortes y posiciones incorrectas tras cambiar designPosition, width o height.

🐛 Reinicio de la matriz y guardado del estado del contexto
       const productImg = await loadImage(productImageUrl);
       const designImg = await loadImage(designImageUrl);
       if (cancelled) return;

+      c.setTransform(1, 0, 0, 1, 0, 0);
+      c.globalAlpha = 1;
+      c.globalCompositeOperation = 'source-over';
       c.clearRect(0, 0, width, height);
       c.fillStyle = '`#f4f2e6`';
       c.fillRect(0, 0, width, height);

       if (productImg) {
         drawCover(c, productImg, width, height);
       }

       if (designImg) {
+        c.save();
         c.globalCompositeOperation =
           designPosition.blend_mode === 'multiply'
             ? 'multiply'
             : ('source-over' as GlobalCompositeOperation);
         c.globalAlpha = designPosition.opacity / 100;
         drawDesign(c, designImg, designPosition, width, height);
+        c.restore();
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;
const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;
c.clearRect(0, 0, width, height);
c.fillStyle = '#f4f2e6';
c.fillRect(0, 0, width, height);
if (productImg) {
drawCover(c, productImg, width, height);
}
if (designImg) {
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
}
}
async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;
const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;
c.setTransform(1, 0, 0, 1, 0, 0);
c.globalAlpha = 1;
c.globalCompositeOperation = 'source-over';
c.clearRect(0, 0, width, height);
c.fillStyle = '`#f4f2e6`';
c.fillRect(0, 0, width, height);
if (productImg) {
drawCover(c, productImg, width, height);
}
if (designImg) {
c.save();
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
c.restore();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/cart/design-preview.tsx around lines 37 - 61, The 2D canvas
context in draw() is being reused without resetting its transform and drawing
state, so stale translate/rotate, globalAlpha, and globalCompositeOperation
values leak into later renders. In app/[locale]/cart/design-preview.tsx, update
draw() around the existing drawCover and drawDesign calls to reset the context
state before repainting and isolate the design-specific state changes inside
drawDesign or with save/restore on the shared context. Ensure the context is
restored to a clean baseline on every invocation of draw() so changing
designPosition, width, or height does not inherit the previous render’s matrix
or compositing settings.

Comment thread app/[locale]/page.module.css Outdated
Comment on lines +15 to +52
background:
radial-gradient(
circle at top left,
rgba(193, 224, 140, 0.22),
transparent 34%
),
rgba(255, 255, 255, 0.94);
border: 1px solid rgba(13, 92, 70, 0.1);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
box-shadow: 0 18px 34px rgba(13, 92, 70, 0.08);
}

.productCard:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-3px);
box-shadow: 0 22px 38px rgba(13, 92, 70, 0.12);
}

.productImageWrap {
position: relative;
aspect-ratio: 1 / 1;
overflow: hidden;
border-radius: 22px;
background:
linear-gradient(
160deg,
rgba(244, 242, 230, 0.96),
rgba(255, 255, 255, 0.86)
),
var(--color-white);
border: 1px solid rgba(13, 92, 70, 0.08);
}

.productImage {
object-fit: contain;
padding: 0.9rem;
filter: drop-shadow(0 14px 18px rgba(13, 92, 70, 0.12));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Errores de Stylelint en las notaciones de color.

Stylelint marca como error las declaraciones rgba(...) con notación legacy y valores alfa decimales (reglas color-function-notation, color-function-alias-notation, alpha-value-notation) en las líneas 18, 21, 22, 26, 42, 43, 46 y 52. Migra a rgb(... / N%) para no romper el lint.

Los avisos de selector-class-pattern (kebab-case) parecen falsos positivos dada la convención camelCase de CSS Modules del repositorio; conviene revisar la configuración de esa regla en lugar de renombrar clases.

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 18-18: Expected "0.22" to be "22%" (alpha-value-notation)

(alpha-value-notation)


[error] 21-21: Expected "0.94" to be "94%" (alpha-value-notation)

(alpha-value-notation)


[error] 22-22: Expected "0.1" to be "10%" (alpha-value-notation)

(alpha-value-notation)


[error] 26-26: Expected "0.08" to be "8%" (alpha-value-notation)

(alpha-value-notation)


[error] 31-31: Expected "0.12" to be "12%" (alpha-value-notation)

(alpha-value-notation)


[error] 42-42: Expected "0.96" to be "96%" (alpha-value-notation)

(alpha-value-notation)


[error] 43-43: Expected "0.86" to be "86%" (alpha-value-notation)

(alpha-value-notation)


[error] 46-46: Expected "0.08" to be "8%" (alpha-value-notation)

(alpha-value-notation)


[error] 52-52: Expected "0.12" to be "12%" (alpha-value-notation)

(alpha-value-notation)


[error] 18-18: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 21-21: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 22-22: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 26-26: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 31-31: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 42-42: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 43-43: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 46-46: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 52-52: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 18-18: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 21-21: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 22-22: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 26-26: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 31-31: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 42-42: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 43-43: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 46-46: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 52-52: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 29-29: Expected class selector ".productCard" to be kebab-case (selector-class-pattern)

(selector-class-pattern)


[error] 34-34: Expected class selector ".productImageWrap" to be kebab-case (selector-class-pattern)

(selector-class-pattern)


[error] 49-49: Expected class selector ".productImage" to be kebab-case (selector-class-pattern)

(selector-class-pattern)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/page.module.css around lines 15 - 52, Update the color
declarations in app/[locale]/page.module.css to satisfy Stylelint by replacing
every legacy rgba(...) usage with modern rgb(... / N%) syntax, especially in the
productCard, productImageWrap, and productImage rules. Keep the existing class
names unchanged, since selector-class-pattern camelCase warnings are likely a
CSS Modules configuration issue and should not be fixed by renaming these
selectors.

Source: Linters/SAST tools

Comment on lines +124 to +131
await fetch(result.uploadUrl, {
method: 'PUT',
headers: { 'content-type': file.type },
body: file,
});

const imageUrl = toAbsoluteUrl(result.publicUrl);
setImage({ imageUploadId: result.id, imageUrl });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

No se verifica el resultado del PUT de subida.

La segunda llamada fetch(result.uploadUrl, ...) no comprueba response.ok. Si el PUT al almacenamiento falla, setImage se ejecuta igualmente con una URL que puede no existir, dejando el draft en un estado inconsistente.

🛡️ Verificar la respuesta del PUT
-    await fetch(result.uploadUrl, {
-      method: 'PUT',
-      headers: { 'content-type': file.type },
-      body: file,
-    });
+    const putResponse = await fetch(result.uploadUrl, {
+      method: 'PUT',
+      headers: { 'content-type': file.type },
+      body: file,
+    });
+
+    if (!putResponse.ok) {
+      throw new Error('Upload failed');
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await fetch(result.uploadUrl, {
method: 'PUT',
headers: { 'content-type': file.type },
body: file,
});
const imageUrl = toAbsoluteUrl(result.publicUrl);
setImage({ imageUploadId: result.id, imageUrl });
const putResponse = await fetch(result.uploadUrl, {
method: 'PUT',
headers: { 'content-type': file.type },
body: file,
});
if (!putResponse.ok) {
throw new Error('Upload failed');
}
const imageUrl = toAbsoluteUrl(result.publicUrl);
setImage({ imageUploadId: result.id, imageUrl });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-experience.tsx around lines 124 -
131, The upload flow in the customization experience does not verify the result
of the PUT request before continuing. In the logic around the result.uploadUrl
fetch and setImage call, check the fetch response.ok and only setImage when the
upload succeeds; if it fails, handle the error and avoid storing the image URL
so the draft stays consistent.

Comment on lines +45 to +47
.colorItem:hover {
border-color: rgba(0, 0, 0, 0.2);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Notación de color obsoleta detectada por Stylelint.

rgba(0, 0, 0, 0.2) debe migrarse a la notación moderna esperada por la configuración de Stylelint del repo.

🎨 Fix sugerido
 .colorItem:hover {
-  border-color: rgba(0, 0, 0, 0.2);
+  border-color: rgb(0 0 0 / 20%);
 }

Los errores de selector-class-pattern (kebab-case) sobre las clases camelCase se omiten aquí por ser una inconsistencia de configuración repo-wide, tal como se estableció en una revisión previa sobre preview-overlay.module.css.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.colorItem:hover {
border-color: rgba(0, 0, 0, 0.2);
}
.colorItem:hover {
border-color: rgb(0 0 0 / 20%);
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 46-46: Expected "0.2" to be "20%" (alpha-value-notation)

(alpha-value-notation)


[error] 46-46: Expected "rgba" to be "rgb" (color-function-alias-notation)

(color-function-alias-notation)


[error] 46-46: Expected modern color-function notation (color-function-notation)

(color-function-notation)


[error] 45-45: Expected class selector ".colorItem" to be kebab-case (selector-class-pattern)

(selector-class-pattern)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-form.module.css around lines 45 -
47, La regla de estilo en .colorItem:hover usa una notación de color obsoleta
que Stylelint rechaza; actualiza el valor de border-color a la sintaxis moderna
esperada por la configuración del repo. Mantén el selector y la intención
visual, cambiando solo la forma de expresar el negro con opacidad en este bloque
de customization-form.module.css.

Source: Linters/SAST tools

Comment on lines +62 to +64
const textErrorId = errors.text ? 'customization-text-error' : undefined;
const _colorErrorId = errors.color ? 'customization-color-error' : undefined;
const sizeErrorId = errors.size ? 'customization-size-error' : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El error de validación de color nunca se muestra al usuario.

_colorErrorId se calcula pero no se usa en la sección de color (93-128), a diferencia de textErrorId y sizeErrorId, que sí se renderizan con role="alert". Si validateDraft() devuelve un error en color, el formulario falla el submit silenciosamente sin ninguna pista visual de qué corregir.

💡 Sugerencia
       {productImages.length > 0 && (
-        <div className={formStyles.colorSection}>
+        <div className={formStyles.colorSection}>
           <span className={formStyles.colorLabel}>
             {labels.customizationColor}
           </span>
           <div className={formStyles.colorCarousel}>
             ...
           </div>
+          {errors.color && (
+            <p id={_colorErrorId} role="alert">
+              {errors.color}
+            </p>
+          )}
         </div>
       )}

Nota: renombrar _colorErrorId a colorErrorId al dejar de ser una variable "no usada".

Dado que el PR forma parte de una serie de PRs apilados con más trabajo de personalización pendiente, esto podría ser intencional/temporal; aun así conviene señalarlo.

Also applies to: 93-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/products/[id]/customization-form.tsx around lines 62 - 64, The
color validation error is computed in customization-form.tsx but never rendered,
so users get no feedback when validateDraft() fails on color. Update the color
section in customization-form.tsx to use the existing error state there: rename
_colorErrorId to colorErrorId and render the color error message with
role="alert" and the proper aria-describedby wiring, matching how textErrorId
and sizeErrorId are handled.

Comment on lines +75 to +81
customization: {
text: item.customizationText ?? null,
color: item.customizationColor ?? null,
size: item.customizationSize ?? null,
imageUrl: item.customizationImageUrl ?? null,
colorImageUrl: item.productImageUrl ?? null,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El carrito de invitado no propaga designPosition.

guestItemToDTO rellena customization sin designPosition, pero el render (líneas 288‑289) condiciona DesignPreview a item.customization?.designPosition. Como GuestCartItem sí guarda customizationDesignPosition, los ítems de invitado con una posición de diseño nunca mostrarán la vista previa; siempre caerán al <img>.

🐛 Mapear designPosition en guestItemToDTO
     customization: {
       text: item.customizationText ?? null,
       color: item.customizationColor ?? null,
       size: item.customizationSize ?? null,
       imageUrl: item.customizationImageUrl ?? null,
       colorImageUrl: item.productImageUrl ?? null,
+      designPosition: item.customizationDesignPosition ?? null,
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
customization: {
text: item.customizationText ?? null,
color: item.customizationColor ?? null,
size: item.customizationSize ?? null,
imageUrl: item.customizationImageUrl ?? null,
colorImageUrl: item.productImageUrl ?? null,
},
customization: {
text: item.customizationText ?? null,
color: item.customizationColor ?? null,
size: item.customizationSize ?? null,
imageUrl: item.customizationImageUrl ?? null,
colorImageUrl: item.productImageUrl ?? null,
designPosition: item.customizationDesignPosition ?? null,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/presentation/components/cart-popup.tsx` around lines 75 - 81,
`guestItemToDTO` is dropping `designPosition` from the `customization` object,
so guest cart items never satisfy the `DesignPreview` condition in the cart
popup. Update `guestItemToDTO` in `cart-popup.tsx` to map
`GuestCartItem.customizationDesignPosition` into `customization.designPosition`,
keeping the existing `customization` shape consistent with the render logic that
checks `item.customization?.designPosition`.

Comment on lines +128 to +142
const productCustomizationConfigSchema = z
.object({
mode: customizationModeSchema.optional(),
previewEnabled: z.boolean().optional(),
previewTemplateUrl: z.string().min(1).nullable().optional(),
sizeOptions: z.array(z.string().min(1)).nullable().optional(),
textOffset: previewOffsetSchema.nullable().optional(),
imageOffset: previewOffsetSchema.nullable().optional(),
allowPhotoDesign: z.boolean().optional(),
designChangeDescription: z.string().nullable().optional(),
categoryId: z.string().nullable().optional(),
tagNames: z.array(z.string().min(1)).nullable().optional(),
designPosition: designPositionSchema.nullable().optional(),
})
.strict();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

El esquema del dominio no replica los límites de longitud/tamaño de la capa de presentación.

designChangeDescription y tagNames no tienen .max(...) aquí, mientras que el esquema equivalente en modules/products/presentation/schemas/product-form-schema.ts sí impone .max(2000) y .max(20) respectivamente. Si ProductCustomizationConfig.fromJson() se invoca desde cualquier punto que no pase por el formulario/esquema de presentación (p. ej. directamente desde un caso de uso con un dto.customizationConfig arbitrario), no habría tope alguno.

Sugiero que el dominio sea la fuente de verdad de estas restricciones y que la capa de presentación las reutilice, en línea con el principio "Domain-First".

Basado en el aprendizaje recuperado: "Keep business logic in the domain layer (Domain-First)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/products/domain/value-objects/product-customization-config.ts` around
lines 128 - 142, The domain schema in product-customization-config.ts is missing
the same length constraints enforced in the presentation schema, so
ProductCustomizationConfig.fromJson() can accept overlong values when called
outside the form flow. Add the max-length limits for designChangeDescription and
tagNames to productCustomizationConfigSchema, and make product-form-schema.ts
reuse that domain schema instead of duplicating the rules so the domain stays
the source of truth.

Source: Learnings

Comment on lines +45 to +121
<label className={styles.field}>
<span>{labels.designChangeDescriptionLabel}</span>
<textarea
value={value.designChangeDescription ?? ''}
onChange={(event) =>
update({
designChangeDescription: event.target.value.trim() || null,
})
}
placeholder={labels.designChangeDescriptionPlaceholder}
rows={3}
/>
</label>

<label className={styles.field}>
<span>{labels.sizeOptionsLabel}</span>
<input
type="text"
value={value.sizeOptions?.join(', ') ?? ''}
onChange={(event) => {
const options = event.target.value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
update({ sizeOptions: options.length > 0 ? options : null });
}}
placeholder={labels.sizeOptionsPlaceholder}
/>
</label>

<label className={styles.field}>
<span>{labels.categoryLabel}</span>
{categories.length > 0 ? (
<select
value={value.categoryId ?? ''}
onChange={(event) =>
update({ categoryId: event.target.value || null })
}
>
<option value="">{labels.categoryPlaceholder}</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
) : (
<input
type="text"
value={value.categoryId ?? ''}
onChange={(event) =>
update({ categoryId: event.target.value || null })
}
placeholder={labels.categoryPlaceholder}
/>
)}
</label>

<label className={styles.field}>
<span>{labels.tagsLabel}</span>
<input
type="text"
value={tagInput}
onChange={(event) => {
const tags = event.target.value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
update({ tagNames: tags.length > 0 ? tags : null });
}}
placeholder={labels.tagsPlaceholder}
aria-describedby="tags-help"
/>
<span id="tags-help" className={styles.help}>
{labels.tagsHelp}
</span>
</label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El recorte/parseo en cada onChange revierte el carácter que el usuario acaba de teclear.

En los tres campos (designChangeDescription, sizeOptions, tagNames) el valor mostrado se recompone siempre desde el estado ya transformado (.trim() o split(',').map(trim).filter(Boolean) seguido de .join(', ')). Esto hace que, al escribir un espacio final o una coma, el carácter desaparezca inmediatamente en el siguiente render porque el value del input vuelve a la versión "limpia". En la práctica, es imposible escribir una descripción de varias palabras o añadir tallas/etiquetas separadas por comas tecleando de forma natural (solo funciona si se pega el texto completo de una vez).

Sugerencia: mantener un estado local "en bruto" (string) por campo mientras se edita, y aplicar el recorte/parseo solo al perder el foco (onBlur) o al construir el payload final (buildPayload), no en cada onChange.

🐛 Ejemplo de solución para el campo de etiquetas
-  const tagInput = useMemo(
-    () => (value.tagNames ?? []).join(', '),
-    [value.tagNames],
-  );
+  const [tagInput, setTagInput] = useState(
+    () => (value.tagNames ?? []).join(', '),
+  );
+
+  useEffect(() => {
+    setTagInput((value.tagNames ?? []).join(', '));
+  }, [value.tagNames]);

   ...

           <input
             type="text"
             value={tagInput}
             onChange={(event) => {
-              const tags = event.target.value
-                .split(',')
-                .map((item) => item.trim())
-                .filter(Boolean);
-              update({ tagNames: tags.length > 0 ? tags : null });
+              setTagInput(event.target.value);
+            }}
+            onBlur={() => {
+              const tags = tagInput
+                .split(',')
+                .map((item) => item.trim())
+                .filter(Boolean);
+              update({ tagNames: tags.length > 0 ? tags : null });
             }}

El mismo patrón aplicaría a sizeOptions y designChangeDescription.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/products/presentation/components/product-customization-config-editor.tsx`
around lines 45 - 121, The current onChange handlers in
product-customization-config-editor.tsx are normalizing the controlled values
too early, so typed spaces and commas get removed immediately and the input
cursor appears to “eat” user keystrokes. Update the fields for
designChangeDescription, sizeOptions, and tagNames to keep a raw string in local
state while editing, and move the trim/split/filter normalization out of the
input handlers into onBlur or the payload-building path. Use the existing
identifiers update, tagInput, and the controlled inputs in
ProductCustomizationConfigEditor to locate the affected logic.

Comment on lines +116 to +120
aria-describedby="tags-help"
/>
<span id="tags-help" className={styles.help}>
{labels.tagsHelp}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

El id "tags-help" está fijo en lugar de generarse con useId().

Si este editor llega a renderizarse más de una vez en la misma página, se duplicaría el id, rompiendo aria-describedby. El propio PR ya usa useId() para este mismo caso en FileUploadDropzone.

♻️ Propuesta
+import { useId, useMemo } from 'react';
...
+  const tagsHelpId = useId();
...
             placeholder={labels.tagsPlaceholder}
-            aria-describedby="tags-help"
+            aria-describedby={tagsHelpId}
           />
-          <span id="tags-help" className={styles.help}>
+          <span id={tagsHelpId} className={styles.help}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
aria-describedby="tags-help"
/>
<span id="tags-help" className={styles.help}>
{labels.tagsHelp}
</span>
aria-describedby={tagsHelpId}
/>
<span id={tagsHelpId} className={styles.help}>
{labels.tagsHelp}
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/products/presentation/components/product-customization-config-editor.tsx`
around lines 116 - 120, The tags help text in
product-customization-config-editor uses a hardcoded id ("tags-help"), which can
collide when the component renders multiple times and break aria-describedby.
Update this editor to generate a unique id with useId(), then use that generated
value for both the aria-describedby on the input and the matching help <span>,
following the same pattern already used in FileUploadDropzone.

Comment thread tmp/check-db.ts
Comment on lines +13 to +15
const users = await prisma.user.findMany();
console.log('--- Users ---');
console.log(users.map((u) => ({ id: u.id, email: u.email, role: u.role })));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Evitar loguear PII (emails) en consola.

Se imprime el email de cada usuario en texto plano. Aunque sea un script de inspección local, si se ejecuta contra una base de datos real y su salida termina en logs de CI/terminal compartida, expone PII.

🔒 Propuesta: enmascarar el email
-  console.log(users.map((u) => ({ id: u.id, email: u.email, role: u.role })));
+  console.log(users.map((u) => ({ id: u.id, role: u.role })));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const users = await prisma.user.findMany();
console.log('--- Users ---');
console.log(users.map((u) => ({ id: u.id, email: u.email, role: u.role })));
const users = await prisma.user.findMany();
console.log('--- Users ---');
console.log(users.map((u) => ({ id: u.id, role: u.role })));
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 14-14: Avoid logging sensitive data
Context: console.log(users.map((u) => ({ id: u.id, email: u.email, role: u.role })))
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tmp/check-db.ts` around lines 13 - 15, The user inspection script in
check-db.ts is logging PII by printing each user’s email in plain text. Update
the user mapping in the console output to avoid exposing the raw email, either
by removing that field or masking it before logging, while keeping the existing
prisma.user.findMany flow and the id/role output intact.

Source: Linters/SAST tools

Developer added 2 commits July 3, 2026 16:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
app/[locale]/seller/products/page.tsx (1)

140-145: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Considera next/link para el enlace "Crear producto".

Navegación interna con <a href> provoca recarga completa de página, perdiendo las transiciones client-side y el estado que Next.js ofrece con next/link para rutas internas. Dado que el resto de la página (paginación) también usa <a> planas, esto podría ser un patrón intencional del módulo; si no lo es, unifica con Link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/page.tsx around lines 140 - 145, The “Crear
producto” navigation in the seller products page uses a plain anchor for an
internal route, so update the create button to use Next.js routing via the
existing page component’s link handling instead of a raw <a>. In
app/[locale]/seller/products/page.tsx, replace the anchor used for the
/seller/products/new destination with next/link so client-side navigation and
preserved state work consistently; keep the same label and locale-aware href
construction.
modules/cart/presentation/components/cart-popup.module.css (1)

195-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Aviso de Stylelint: nombres de clase no kebab-case.

Stylelint marca .checkoutBtn como no kebab-case; el mismo patrón camelCase se repite en todo el archivo (.cta, .itemInfo, .fullCartBtn) y también en cart-view.module.css (.ctaButton, .itemDetails). Si el proyecto usa CSS Modules con camelCase intencionalmente para el acceso styles.xxx en JS, sería más adecuado ajustar la regla selector-class-pattern de Stylelint en vez de renombrar cada clase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-popup.module.css` around lines 195
- 208, The Stylelint warning comes from the class naming rule, and the CSS
Modules files intentionally use camelCase selectors like checkoutBtn, cta,
itemInfo, fullCartBtn, ctaButton, and itemDetails for styles.xxx access. Update
the Stylelint selector-class-pattern configuration to allow this naming
convention instead of renaming the classes in cart-popup.module.css and
cart-view.module.css.

Source: Linters/SAST tools

modules/cart/presentation/components/cart-view.tsx (1)

331-349: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

buildCustomizationHref se invoca dos veces para el mismo ítem.

Se llama una vez para comprobar si existe href y otra para obtener el valor; basta con calcularlo una sola vez en una variable local.

♻️ Calcular una sola vez
-                {labels.customizationEditFromCart &&
-                  buildCustomizationHref(
-                    locale,
-                    item.productId,
-                    item.customization,
-                  ) && (
-                    <a
-                      href={
-                        buildCustomizationHref(
-                          locale,
-                          item.productId,
-                          item.customization,
-                        ) ?? '#'
-                      }
-                      className={styles.editLink}
-                    >
-                      {labels.customizationEditFromCart}
-                    </a>
-                  )}
+                {(() => {
+                  const href = buildCustomizationHref(
+                    locale,
+                    item.productId,
+                    item.customization,
+                  );
+                  return (
+                    labels.customizationEditFromCart &&
+                    href && (
+                      <a href={href} className={styles.editLink}>
+                        {labels.customizationEditFromCart}
+                      </a>
+                    )
+                  );
+                })()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-view.tsx` around lines 331 - 349,
`buildCustomizationHref` is being called twice for the same cart item in
`CartView`, once in the conditional and again for the anchor `href`. Compute the
result once in a local variable near the `labels.customizationEditFromCart`
block, then use that variable for both the existence check and the `href` value
to avoid duplicate work.
modules/cart/presentation/components/cart-popup.tsx (1)

139-151: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Doble cast as unknown as DesignPositionData sin validación de forma.

customization.designPosition se tipa como Record<string, unknown> | null y luego se fuerza a DesignPositionData vía doble cast antes de pasarlo a DesignPreview, que usa directamente designPosition.blend_mode/.opacity para el canvas. Si el payload de /api/cart no coincide exactamente con la forma esperada, esto puede degradar silenciosamente el render (p. ej. globalAlpha en NaN) sin ningún aviso en tiempo de compilación ni runtime. cart-view.tsx ya define customization.designPosition con la forma concreta (imageUrl, x, y, scale, rotation_deg, opacity, blend_mode) en vez de Record<string, unknown>; alinear este archivo con ese tipo (o con el esquema Zod de CustomizationOptions del dominio) eliminaría el cast inseguro.

Also applies to: 294-306

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-popup.tsx` around lines 139 - 151,
The `cart-popup.tsx` customization mapping is using an unsafe double cast for
`designPosition`, which can hide shape mismatches before `DesignPreview` reads
fields like `blend_mode` and `opacity`. Update the `cart-item` transformation in
`cart-popup.tsx` to use the same concrete `CustomizationOptions.designPosition`
shape as `cart-view.tsx` (or validate against the `CustomizationOptions`/Zod
schema) instead of `Record<string, unknown>`, and remove the `as unknown as
DesignPositionData` cast so the data shape is checked before rendering.
tests/unit/components/cart/add-to-cart-button.test.tsx (1)

355-373: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Falta cobertura para el matching por designPosition.

Dado el bug señalado en add-to-cart-button.tsx (el matching ignora designPosition), sería valioso añadir un caso que verifique que dos personalizaciones con el mismo texto/imagen pero distinta designPosition se traten como ítems de carrito distintos.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/components/cart/add-to-cart-button.test.tsx` around lines 355 -
373, Add a test in add-to-cart-button.test.tsx to cover the cart-item matching
bug around designPosition: create two customizations with the same text/image
but different designPosition values and verify AddToCartButton treats them as
separate items instead of matching them together. Reuse the existing
AddToCartButton render/setup and assert the expected quantity/update behavior
through the relevant updateQuantity flow so the regression is caught.
modules/cart/presentation/components/add-to-cart-choice-modal.module.css (1)

7-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Notación de color obsoleta según Stylelint.

El linter reporta uso de rgba(...) con alfa decimal en varias líneas (10, 15, 19, 24, 41, 61); debería usarse la notación moderna rgb(r g b / a%).

🎨 Propuesta de fix
   background:
     radial-gradient(
       circle at top right,
-      rgba(223, 128, 114, 0.14),
+      rgb(223 128 114 / 14%),
       transparent 42%
     ),
     radial-gradient(
       circle at bottom left,
-      rgba(193, 224, 140, 0.2),
+      rgb(193 224 140 / 20%),
       transparent 38%
     ),
     var(--color-white);
-  box-shadow: 0 28px 60px rgba(13, 92, 70, 0.18);
+  box-shadow: 0 28px 60px rgb(13 92 70 / 18%);
 .kicker {
   margin: 0;
-  color: rgba(13, 92, 70, 0.65);
+  color: rgb(13 92 70 / 65%);
 .message {
   margin: 0;
-  color: rgba(0, 0, 0, 0.72);
+  color: rgb(0 0 0 / 72%);
   border: 1px solid var(--color-green-dark);
-  box-shadow: 0 10px 22px rgba(13, 92, 70, 0.16);
+  box-shadow: 0 10px 22px rgb(13 92 70 / 16%);

Also applies to: 24-24, 39-43, 56-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/add-to-cart-choice-modal.module.css`
around lines 7 - 19, Stylelint is flagging deprecated color notation in
add-to-cart-choice-modal.module.css; replace every rgba(...) alpha usage in the
modal styles with modern rgb(r g b / a%) notation. Update the background
gradients and shadow definitions in the affected rules so the component keeps
the same visual appearance while matching the linter’s required syntax.

Source: Linters/SAST tools

modules/cart/presentation/components/customization-draft-schema.ts (1)

1-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolida components/cart/customization-draft-schema.ts y components/cart/add-to-cart-choice-modal.tsx.
Ahora duplican exactamente modules/cart/presentation/components/*; conviértelos en re-exports o elimínalos para evitar divergencias futuras. components/cart/add-to-cart-button.tsx ya sigue ese patrón.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/customization-draft-schema.ts` around
lines 1 - 54, The customization draft schema is duplicated between the
presentation module and the legacy components entrypoints, so update the legacy
files to re-export the shared implementation instead of copying it. Align the
symbols customizatonDraftSchema, designPositionSchema, and
normalizeCustomizationDraft so the canonical definitions stay in
modules/cart/presentation/components/customization-draft-schema.ts, and make
components/cart/add-to-cart-choice-modal.tsx follow the same re-export pattern
already used by add-to-cart-button.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/cart/presentation/components/add-to-cart-button.tsx`:
- Around line 55-102: The customization matching logic in
guestCustomizationMatches and authCustomizationMatches is ignoring
designPosition when comparing a draft to existing cart items, which can collapse
distinct customizations into one. Update the comparison in both helpers to
include designPosition alongside text, color, size, and imageUrl, and make sure
the authCustomizationMatches path uses the same full equality check rather than
only using designPosition as a content flag.

In `@modules/cart/presentation/components/cart-view.tsx`:
- Around line 298-304: The cart view still has hardcoded Spanish strings for the
customization section, which breaks i18n for other locales. Update the affected
text in CartView to read from the existing labels prop instead of inline
literals, using the same pattern already used for soldBy, remove, subtotal, and
checkout. Check the customization rendering in cart-view.tsx (including the
DesignThumb/image block and the related style/price labels) and replace the
fixed strings with label keys so the locale-specific copy comes from labels.
- Around line 142-163: CartView is still identifying guest cart entries by
productId instead of the guest item id, which causes duplicate keys and wrong
quantity/removal behavior for multiple customizations of the same product.
Update the guestCart.items mapping in CartView so each guest entry uses its own
id field from gi, and adjust the item action handlers to call updateQuantity and
removeItem with that item id rather than productId. Keep the authenticated path
unchanged and ensure the unique guest id is used consistently wherever
CartItemDTO is rendered or acted on.

In `@shared/ui/file-upload-dropzone.tsx`:
- Around line 1-165: This dropzone is duplicated in another shared component, so
consolidate the implementation into a single canonical FileUploadDropzone and
remove the extra copy. Move or reuse the shared symbols FileUploadDropzoneItem,
FileUploadDropzoneProps, handleFiles, and FileUploadDropzone from the shared
location, then update all consumers such as ProductPhotoGallery and
PhotoUploadField to import the same component path.

---

Nitpick comments:
In `@app/`[locale]/seller/products/page.tsx:
- Around line 140-145: The “Crear producto” navigation in the seller products
page uses a plain anchor for an internal route, so update the create button to
use Next.js routing via the existing page component’s link handling instead of a
raw <a>. In app/[locale]/seller/products/page.tsx, replace the anchor used for
the /seller/products/new destination with next/link so client-side navigation
and preserved state work consistently; keep the same label and locale-aware href
construction.

In `@modules/cart/presentation/components/add-to-cart-choice-modal.module.css`:
- Around line 7-19: Stylelint is flagging deprecated color notation in
add-to-cart-choice-modal.module.css; replace every rgba(...) alpha usage in the
modal styles with modern rgb(r g b / a%) notation. Update the background
gradients and shadow definitions in the affected rules so the component keeps
the same visual appearance while matching the linter’s required syntax.

In `@modules/cart/presentation/components/cart-popup.module.css`:
- Around line 195-208: The Stylelint warning comes from the class naming rule,
and the CSS Modules files intentionally use camelCase selectors like
checkoutBtn, cta, itemInfo, fullCartBtn, ctaButton, and itemDetails for
styles.xxx access. Update the Stylelint selector-class-pattern configuration to
allow this naming convention instead of renaming the classes in
cart-popup.module.css and cart-view.module.css.

In `@modules/cart/presentation/components/cart-popup.tsx`:
- Around line 139-151: The `cart-popup.tsx` customization mapping is using an
unsafe double cast for `designPosition`, which can hide shape mismatches before
`DesignPreview` reads fields like `blend_mode` and `opacity`. Update the
`cart-item` transformation in `cart-popup.tsx` to use the same concrete
`CustomizationOptions.designPosition` shape as `cart-view.tsx` (or validate
against the `CustomizationOptions`/Zod schema) instead of `Record<string,
unknown>`, and remove the `as unknown as DesignPositionData` cast so the data
shape is checked before rendering.

In `@modules/cart/presentation/components/cart-view.tsx`:
- Around line 331-349: `buildCustomizationHref` is being called twice for the
same cart item in `CartView`, once in the conditional and again for the anchor
`href`. Compute the result once in a local variable near the
`labels.customizationEditFromCart` block, then use that variable for both the
existence check and the `href` value to avoid duplicate work.

In `@modules/cart/presentation/components/customization-draft-schema.ts`:
- Around line 1-54: The customization draft schema is duplicated between the
presentation module and the legacy components entrypoints, so update the legacy
files to re-export the shared implementation instead of copying it. Align the
symbols customizatonDraftSchema, designPositionSchema, and
normalizeCustomizationDraft so the canonical definitions stay in
modules/cart/presentation/components/customization-draft-schema.ts, and make
components/cart/add-to-cart-choice-modal.tsx follow the same re-export pattern
already used by add-to-cart-button.tsx.

In `@tests/unit/components/cart/add-to-cart-button.test.tsx`:
- Around line 355-373: Add a test in add-to-cart-button.test.tsx to cover the
cart-item matching bug around designPosition: create two customizations with the
same text/image but different designPosition values and verify AddToCartButton
treats them as separate items instead of matching them together. Reuse the
existing AddToCartButton render/setup and assert the expected quantity/update
behavior through the relevant updateQuantity flow so the regression is caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a39c5198-5261-4a72-97ec-a8b9b0db0bc2

📥 Commits

Reviewing files that changed from the base of the PR and between c26333c and baff07d.

⛔ Files ignored due to path filters (4)
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/43150e8a-f92c-4d99-b36f-9ad8b12c92fd.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/8299071a-2631-4a3e-bc8f-cdba90a76c3d.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/c2421aa8-a506-4e1e-b738-e47e64d8956c.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr4su2n40001hstky222wdt1/0b154df8-be9c-4f79-a72c-08c31981c3d1.png is excluded by !**/*.png
📒 Files selected for processing (34)
  • .gitignore
  • AGENTS.md
  • app/[locale]/cart/cart-view.tsx
  • app/[locale]/checkout/page.tsx
  • app/[locale]/products/[id]/customization-experience.tsx
  • app/[locale]/seller/products/page.module.css
  • app/[locale]/seller/products/page.tsx
  • components/cart/add-to-cart-button.tsx
  • components/cart/add-to-cart-choice-modal.tsx
  • modules/cart/presentation/components/add-to-cart-button.module.css
  • modules/cart/presentation/components/add-to-cart-button.tsx
  • modules/cart/presentation/components/add-to-cart-choice-modal.module.css
  • modules/cart/presentation/components/add-to-cart-choice-modal.tsx
  • modules/cart/presentation/components/cart-popup.module.css
  • modules/cart/presentation/components/cart-popup.tsx
  • modules/cart/presentation/components/cart-view.module.css
  • modules/cart/presentation/components/cart-view.tsx
  • modules/cart/presentation/components/customization-draft-schema.ts
  • modules/products/infrastructure/prisma-product-repository.ts
  • prisma/schema.prisma
  • prisma/seed.ts
  • shared/i18n/locales/cat.json
  • shared/i18n/locales/es.json
  • shared/ui/file-upload-dropzone.module.css
  • shared/ui/file-upload-dropzone.tsx
  • tests/e2e/products/products.spec.ts
  • tests/unit/app/[locale]/cart/cart-view-customization.test.tsx
  • tests/unit/app/[locale]/page.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/app/[locale]/products/[id]/page.test.tsx
  • tests/unit/app/[locale]/seller/products/page.test.tsx
  • tests/unit/app/cart/cart-view.test.tsx
  • tests/unit/components/cart/add-to-cart-button.test.tsx
  • tests/unit/modules/presentation/components/cart-icon.test.tsx
💤 Files with no reviewable changes (1)
  • AGENTS.md
✅ Files skipped from review due to trivial changes (4)
  • components/cart/add-to-cart-button.tsx
  • .gitignore
  • app/[locale]/cart/cart-view.tsx
  • shared/i18n/locales/cat.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • components/cart/add-to-cart-choice-modal.tsx
  • tests/e2e/products/products.spec.ts
  • tests/unit/app/[locale]/cart/cart-view-customization.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/modules/presentation/components/cart-icon.test.tsx
  • app/[locale]/checkout/page.tsx
  • modules/products/infrastructure/prisma-product-repository.ts
  • shared/i18n/locales/es.json
  • prisma/schema.prisma
  • app/[locale]/products/[id]/customization-experience.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (7)
app/[locale]/seller/products/page.tsx (1)

140-145: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Considera next/link para el enlace "Crear producto".

Navegación interna con <a href> provoca recarga completa de página, perdiendo las transiciones client-side y el estado que Next.js ofrece con next/link para rutas internas. Dado que el resto de la página (paginación) también usa <a> planas, esto podría ser un patrón intencional del módulo; si no lo es, unifica con Link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/seller/products/page.tsx around lines 140 - 145, The “Crear
producto” navigation in the seller products page uses a plain anchor for an
internal route, so update the create button to use Next.js routing via the
existing page component’s link handling instead of a raw <a>. In
app/[locale]/seller/products/page.tsx, replace the anchor used for the
/seller/products/new destination with next/link so client-side navigation and
preserved state work consistently; keep the same label and locale-aware href
construction.
modules/cart/presentation/components/cart-popup.module.css (1)

195-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Aviso de Stylelint: nombres de clase no kebab-case.

Stylelint marca .checkoutBtn como no kebab-case; el mismo patrón camelCase se repite en todo el archivo (.cta, .itemInfo, .fullCartBtn) y también en cart-view.module.css (.ctaButton, .itemDetails). Si el proyecto usa CSS Modules con camelCase intencionalmente para el acceso styles.xxx en JS, sería más adecuado ajustar la regla selector-class-pattern de Stylelint en vez de renombrar cada clase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-popup.module.css` around lines 195
- 208, The Stylelint warning comes from the class naming rule, and the CSS
Modules files intentionally use camelCase selectors like checkoutBtn, cta,
itemInfo, fullCartBtn, ctaButton, and itemDetails for styles.xxx access. Update
the Stylelint selector-class-pattern configuration to allow this naming
convention instead of renaming the classes in cart-popup.module.css and
cart-view.module.css.

Source: Linters/SAST tools

modules/cart/presentation/components/cart-view.tsx (1)

331-349: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

buildCustomizationHref se invoca dos veces para el mismo ítem.

Se llama una vez para comprobar si existe href y otra para obtener el valor; basta con calcularlo una sola vez en una variable local.

♻️ Calcular una sola vez
-                {labels.customizationEditFromCart &&
-                  buildCustomizationHref(
-                    locale,
-                    item.productId,
-                    item.customization,
-                  ) && (
-                    <a
-                      href={
-                        buildCustomizationHref(
-                          locale,
-                          item.productId,
-                          item.customization,
-                        ) ?? '#'
-                      }
-                      className={styles.editLink}
-                    >
-                      {labels.customizationEditFromCart}
-                    </a>
-                  )}
+                {(() => {
+                  const href = buildCustomizationHref(
+                    locale,
+                    item.productId,
+                    item.customization,
+                  );
+                  return (
+                    labels.customizationEditFromCart &&
+                    href && (
+                      <a href={href} className={styles.editLink}>
+                        {labels.customizationEditFromCart}
+                      </a>
+                    )
+                  );
+                })()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-view.tsx` around lines 331 - 349,
`buildCustomizationHref` is being called twice for the same cart item in
`CartView`, once in the conditional and again for the anchor `href`. Compute the
result once in a local variable near the `labels.customizationEditFromCart`
block, then use that variable for both the existence check and the `href` value
to avoid duplicate work.
modules/cart/presentation/components/cart-popup.tsx (1)

139-151: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Doble cast as unknown as DesignPositionData sin validación de forma.

customization.designPosition se tipa como Record<string, unknown> | null y luego se fuerza a DesignPositionData vía doble cast antes de pasarlo a DesignPreview, que usa directamente designPosition.blend_mode/.opacity para el canvas. Si el payload de /api/cart no coincide exactamente con la forma esperada, esto puede degradar silenciosamente el render (p. ej. globalAlpha en NaN) sin ningún aviso en tiempo de compilación ni runtime. cart-view.tsx ya define customization.designPosition con la forma concreta (imageUrl, x, y, scale, rotation_deg, opacity, blend_mode) en vez de Record<string, unknown>; alinear este archivo con ese tipo (o con el esquema Zod de CustomizationOptions del dominio) eliminaría el cast inseguro.

Also applies to: 294-306

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-popup.tsx` around lines 139 - 151,
The `cart-popup.tsx` customization mapping is using an unsafe double cast for
`designPosition`, which can hide shape mismatches before `DesignPreview` reads
fields like `blend_mode` and `opacity`. Update the `cart-item` transformation in
`cart-popup.tsx` to use the same concrete `CustomizationOptions.designPosition`
shape as `cart-view.tsx` (or validate against the `CustomizationOptions`/Zod
schema) instead of `Record<string, unknown>`, and remove the `as unknown as
DesignPositionData` cast so the data shape is checked before rendering.
tests/unit/components/cart/add-to-cart-button.test.tsx (1)

355-373: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Falta cobertura para el matching por designPosition.

Dado el bug señalado en add-to-cart-button.tsx (el matching ignora designPosition), sería valioso añadir un caso que verifique que dos personalizaciones con el mismo texto/imagen pero distinta designPosition se traten como ítems de carrito distintos.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/components/cart/add-to-cart-button.test.tsx` around lines 355 -
373, Add a test in add-to-cart-button.test.tsx to cover the cart-item matching
bug around designPosition: create two customizations with the same text/image
but different designPosition values and verify AddToCartButton treats them as
separate items instead of matching them together. Reuse the existing
AddToCartButton render/setup and assert the expected quantity/update behavior
through the relevant updateQuantity flow so the regression is caught.
modules/cart/presentation/components/add-to-cart-choice-modal.module.css (1)

7-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Notación de color obsoleta según Stylelint.

El linter reporta uso de rgba(...) con alfa decimal en varias líneas (10, 15, 19, 24, 41, 61); debería usarse la notación moderna rgb(r g b / a%).

🎨 Propuesta de fix
   background:
     radial-gradient(
       circle at top right,
-      rgba(223, 128, 114, 0.14),
+      rgb(223 128 114 / 14%),
       transparent 42%
     ),
     radial-gradient(
       circle at bottom left,
-      rgba(193, 224, 140, 0.2),
+      rgb(193 224 140 / 20%),
       transparent 38%
     ),
     var(--color-white);
-  box-shadow: 0 28px 60px rgba(13, 92, 70, 0.18);
+  box-shadow: 0 28px 60px rgb(13 92 70 / 18%);
 .kicker {
   margin: 0;
-  color: rgba(13, 92, 70, 0.65);
+  color: rgb(13 92 70 / 65%);
 .message {
   margin: 0;
-  color: rgba(0, 0, 0, 0.72);
+  color: rgb(0 0 0 / 72%);
   border: 1px solid var(--color-green-dark);
-  box-shadow: 0 10px 22px rgba(13, 92, 70, 0.16);
+  box-shadow: 0 10px 22px rgb(13 92 70 / 16%);

Also applies to: 24-24, 39-43, 56-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/add-to-cart-choice-modal.module.css`
around lines 7 - 19, Stylelint is flagging deprecated color notation in
add-to-cart-choice-modal.module.css; replace every rgba(...) alpha usage in the
modal styles with modern rgb(r g b / a%) notation. Update the background
gradients and shadow definitions in the affected rules so the component keeps
the same visual appearance while matching the linter’s required syntax.

Source: Linters/SAST tools

modules/cart/presentation/components/customization-draft-schema.ts (1)

1-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolida components/cart/customization-draft-schema.ts y components/cart/add-to-cart-choice-modal.tsx.
Ahora duplican exactamente modules/cart/presentation/components/*; conviértelos en re-exports o elimínalos para evitar divergencias futuras. components/cart/add-to-cart-button.tsx ya sigue ese patrón.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/customization-draft-schema.ts` around
lines 1 - 54, The customization draft schema is duplicated between the
presentation module and the legacy components entrypoints, so update the legacy
files to re-export the shared implementation instead of copying it. Align the
symbols customizatonDraftSchema, designPositionSchema, and
normalizeCustomizationDraft so the canonical definitions stay in
modules/cart/presentation/components/customization-draft-schema.ts, and make
components/cart/add-to-cart-choice-modal.tsx follow the same re-export pattern
already used by add-to-cart-button.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/cart/presentation/components/add-to-cart-button.tsx`:
- Around line 55-102: The customization matching logic in
guestCustomizationMatches and authCustomizationMatches is ignoring
designPosition when comparing a draft to existing cart items, which can collapse
distinct customizations into one. Update the comparison in both helpers to
include designPosition alongside text, color, size, and imageUrl, and make sure
the authCustomizationMatches path uses the same full equality check rather than
only using designPosition as a content flag.

In `@modules/cart/presentation/components/cart-view.tsx`:
- Around line 298-304: The cart view still has hardcoded Spanish strings for the
customization section, which breaks i18n for other locales. Update the affected
text in CartView to read from the existing labels prop instead of inline
literals, using the same pattern already used for soldBy, remove, subtotal, and
checkout. Check the customization rendering in cart-view.tsx (including the
DesignThumb/image block and the related style/price labels) and replace the
fixed strings with label keys so the locale-specific copy comes from labels.
- Around line 142-163: CartView is still identifying guest cart entries by
productId instead of the guest item id, which causes duplicate keys and wrong
quantity/removal behavior for multiple customizations of the same product.
Update the guestCart.items mapping in CartView so each guest entry uses its own
id field from gi, and adjust the item action handlers to call updateQuantity and
removeItem with that item id rather than productId. Keep the authenticated path
unchanged and ensure the unique guest id is used consistently wherever
CartItemDTO is rendered or acted on.

In `@shared/ui/file-upload-dropzone.tsx`:
- Around line 1-165: This dropzone is duplicated in another shared component, so
consolidate the implementation into a single canonical FileUploadDropzone and
remove the extra copy. Move or reuse the shared symbols FileUploadDropzoneItem,
FileUploadDropzoneProps, handleFiles, and FileUploadDropzone from the shared
location, then update all consumers such as ProductPhotoGallery and
PhotoUploadField to import the same component path.

---

Nitpick comments:
In `@app/`[locale]/seller/products/page.tsx:
- Around line 140-145: The “Crear producto” navigation in the seller products
page uses a plain anchor for an internal route, so update the create button to
use Next.js routing via the existing page component’s link handling instead of a
raw <a>. In app/[locale]/seller/products/page.tsx, replace the anchor used for
the /seller/products/new destination with next/link so client-side navigation
and preserved state work consistently; keep the same label and locale-aware href
construction.

In `@modules/cart/presentation/components/add-to-cart-choice-modal.module.css`:
- Around line 7-19: Stylelint is flagging deprecated color notation in
add-to-cart-choice-modal.module.css; replace every rgba(...) alpha usage in the
modal styles with modern rgb(r g b / a%) notation. Update the background
gradients and shadow definitions in the affected rules so the component keeps
the same visual appearance while matching the linter’s required syntax.

In `@modules/cart/presentation/components/cart-popup.module.css`:
- Around line 195-208: The Stylelint warning comes from the class naming rule,
and the CSS Modules files intentionally use camelCase selectors like
checkoutBtn, cta, itemInfo, fullCartBtn, ctaButton, and itemDetails for
styles.xxx access. Update the Stylelint selector-class-pattern configuration to
allow this naming convention instead of renaming the classes in
cart-popup.module.css and cart-view.module.css.

In `@modules/cart/presentation/components/cart-popup.tsx`:
- Around line 139-151: The `cart-popup.tsx` customization mapping is using an
unsafe double cast for `designPosition`, which can hide shape mismatches before
`DesignPreview` reads fields like `blend_mode` and `opacity`. Update the
`cart-item` transformation in `cart-popup.tsx` to use the same concrete
`CustomizationOptions.designPosition` shape as `cart-view.tsx` (or validate
against the `CustomizationOptions`/Zod schema) instead of `Record<string,
unknown>`, and remove the `as unknown as DesignPositionData` cast so the data
shape is checked before rendering.

In `@modules/cart/presentation/components/cart-view.tsx`:
- Around line 331-349: `buildCustomizationHref` is being called twice for the
same cart item in `CartView`, once in the conditional and again for the anchor
`href`. Compute the result once in a local variable near the
`labels.customizationEditFromCart` block, then use that variable for both the
existence check and the `href` value to avoid duplicate work.

In `@modules/cart/presentation/components/customization-draft-schema.ts`:
- Around line 1-54: The customization draft schema is duplicated between the
presentation module and the legacy components entrypoints, so update the legacy
files to re-export the shared implementation instead of copying it. Align the
symbols customizatonDraftSchema, designPositionSchema, and
normalizeCustomizationDraft so the canonical definitions stay in
modules/cart/presentation/components/customization-draft-schema.ts, and make
components/cart/add-to-cart-choice-modal.tsx follow the same re-export pattern
already used by add-to-cart-button.tsx.

In `@tests/unit/components/cart/add-to-cart-button.test.tsx`:
- Around line 355-373: Add a test in add-to-cart-button.test.tsx to cover the
cart-item matching bug around designPosition: create two customizations with the
same text/image but different designPosition values and verify AddToCartButton
treats them as separate items instead of matching them together. Reuse the
existing AddToCartButton render/setup and assert the expected quantity/update
behavior through the relevant updateQuantity flow so the regression is caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a39c5198-5261-4a72-97ec-a8b9b0db0bc2

📥 Commits

Reviewing files that changed from the base of the PR and between c26333c and baff07d.

⛔ Files ignored due to path filters (4)
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/43150e8a-f92c-4d99-b36f-9ad8b12c92fd.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/8299071a-2631-4a3e-bc8f-cdba90a76c3d.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/c2421aa8-a506-4e1e-b738-e47e64d8956c.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr4su2n40001hstky222wdt1/0b154df8-be9c-4f79-a72c-08c31981c3d1.png is excluded by !**/*.png
📒 Files selected for processing (34)
  • .gitignore
  • AGENTS.md
  • app/[locale]/cart/cart-view.tsx
  • app/[locale]/checkout/page.tsx
  • app/[locale]/products/[id]/customization-experience.tsx
  • app/[locale]/seller/products/page.module.css
  • app/[locale]/seller/products/page.tsx
  • components/cart/add-to-cart-button.tsx
  • components/cart/add-to-cart-choice-modal.tsx
  • modules/cart/presentation/components/add-to-cart-button.module.css
  • modules/cart/presentation/components/add-to-cart-button.tsx
  • modules/cart/presentation/components/add-to-cart-choice-modal.module.css
  • modules/cart/presentation/components/add-to-cart-choice-modal.tsx
  • modules/cart/presentation/components/cart-popup.module.css
  • modules/cart/presentation/components/cart-popup.tsx
  • modules/cart/presentation/components/cart-view.module.css
  • modules/cart/presentation/components/cart-view.tsx
  • modules/cart/presentation/components/customization-draft-schema.ts
  • modules/products/infrastructure/prisma-product-repository.ts
  • prisma/schema.prisma
  • prisma/seed.ts
  • shared/i18n/locales/cat.json
  • shared/i18n/locales/es.json
  • shared/ui/file-upload-dropzone.module.css
  • shared/ui/file-upload-dropzone.tsx
  • tests/e2e/products/products.spec.ts
  • tests/unit/app/[locale]/cart/cart-view-customization.test.tsx
  • tests/unit/app/[locale]/page.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/app/[locale]/products/[id]/page.test.tsx
  • tests/unit/app/[locale]/seller/products/page.test.tsx
  • tests/unit/app/cart/cart-view.test.tsx
  • tests/unit/components/cart/add-to-cart-button.test.tsx
  • tests/unit/modules/presentation/components/cart-icon.test.tsx
💤 Files with no reviewable changes (1)
  • AGENTS.md
✅ Files skipped from review due to trivial changes (4)
  • components/cart/add-to-cart-button.tsx
  • .gitignore
  • app/[locale]/cart/cart-view.tsx
  • shared/i18n/locales/cat.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • components/cart/add-to-cart-choice-modal.tsx
  • tests/e2e/products/products.spec.ts
  • tests/unit/app/[locale]/cart/cart-view-customization.test.tsx
  • tests/unit/app/[locale]/products/[id]/customization-experience.test.tsx
  • tests/unit/modules/presentation/components/cart-icon.test.tsx
  • app/[locale]/checkout/page.tsx
  • modules/products/infrastructure/prisma-product-repository.ts
  • shared/i18n/locales/es.json
  • prisma/schema.prisma
  • app/[locale]/products/[id]/customization-experience.tsx
🛑 Comments failed to post (4)
modules/cart/presentation/components/add-to-cart-button.tsx (1)

55-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El matching de personalizaciones no considera designPosition.

guestCustomizationMatches y authCustomizationMatches comparan text, color, size e imageUrl, pero ignoran designPosition en la comparación real (aunque sí se usa para decidir si el borrador "tiene contenido" en authCustomizationMatches/customizationHasContent). Dos personalizaciones con el mismo texto/imagen pero distinta colocación de diseño se tratarán como el mismo ítem de carrito, fusionando cantidades o marcando incorrectamente "ya en el carrito".

🐛 Propuesta de fix
 function guestCustomizationMatches(
   item: {
     customizationText?: string | null;
     customizationColor?: string | null;
     customizationSize?: string | null;
     customizationImageUrl?: string | null;
+    customizationDesignPosition?: DesignPositionPayload | null;
   },
   draft: CustomizationDraftPayload | null,
 ): boolean {
   const norm = normalizeCustomizationDraft(draft);
   return (
     (item.customizationText ?? null) === (norm.text ?? null) &&
     (item.customizationColor ?? null) === (norm.color ?? null) &&
     (item.customizationSize ?? null) === (norm.size ?? null) &&
-    (item.customizationImageUrl ?? null) === (norm.imageUrl ?? null)
+    (item.customizationImageUrl ?? null) === (norm.imageUrl ?? null) &&
+    JSON.stringify(item.customizationDesignPosition ?? null) ===
+      JSON.stringify(norm.designPosition ?? null)
   );
 }

Aplicar el mismo criterio en authCustomizationMatches.

Also applies to: 150-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/add-to-cart-button.tsx` around lines 55
- 102, The customization matching logic in guestCustomizationMatches and
authCustomizationMatches is ignoring designPosition when comparing a draft to
existing cart items, which can collapse distinct customizations into one. Update
the comparison in both helpers to include designPosition alongside text, color,
size, and imageUrl, and make sure the authCustomizationMatches path uses the
same full equality check rather than only using designPosition as a content
flag.
modules/cart/presentation/components/cart-view.tsx (2)

142-163: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a guest-cart-context.tsx
rg -n "interface GuestCartItem" -A 20 modules/cart/presentation/guest-cart-context.tsx
rg -n "updateItemQuantity|removeItemById|updateQuantity\(|removeItem\(" modules/cart/presentation/guest-cart-context.tsx

Repository: henri318/728-store

Length of output: 1461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== cart-view.tsx (contexto relevante) ==\n'
nl -ba modules/cart/presentation/components/cart-view.tsx | sed -n '130,240p'

printf '\n== guest-cart-context.tsx (API e implementación relevante) ==\n'
nl -ba modules/cart/presentation/guest-cart-context.tsx | sed -n '1,360p'

printf '\n== cart-popup.tsx (comparación) ==\n'
nl -ba modules/cart/presentation/components/cart-popup.tsx | sed -n '1,280p'

Repository: henri318/728-store

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== cart-view.tsx (líneas relevantes) ==\n'
sed -n '130,240p' modules/cart/presentation/components/cart-view.tsx | cat -n

printf '\n== guest-cart-context.tsx (API e implementación relevantes) ==\n'
sed -n '1,360p' modules/cart/presentation/guest-cart-context.tsx | cat -n

printf '\n== cart-popup.tsx (líneas relevantes) ==\n'
sed -n '1,280p' modules/cart/presentation/components/cart-popup.tsx | cat -n

Repository: henri318/728-store

Length of output: 27709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== cart-view.tsx (render y claves) ==\n'
sed -n '240,420p' modules/cart/presentation/components/cart-view.tsx | cat -n

printf '\n== usos de GuestCartContext en cart-view / popup / add-to-cart ==\n'
rg -n "useGuestCart\\(|addItem\\(|updateItemQuantity\\(|removeItemById\\(|updateQuantity\\(|removeItem\\(" modules/cart/presentation -g '!**/*.map'

Repository: henri318/728-store

Length of output: 9272


CartView debe identificar los ítems de invitado por id, no por productId.

guestCart.items ya puede contener varias entradas del mismo producto con ids distintos. Aquí se sigue mapeando id: gi.productId y se llaman updateQuantity(item.productId, ...) / removeItem(item.productId), así que dos personalizaciones del mismo producto se renderizarán con la misma key y las acciones de cantidad/eliminación afectarán a todas las coincidencias.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-view.tsx` around lines 142 - 163,
CartView is still identifying guest cart entries by productId instead of the
guest item id, which causes duplicate keys and wrong quantity/removal behavior
for multiple customizations of the same product. Update the guestCart.items
mapping in CartView so each guest entry uses its own id field from gi, and
adjust the item action handlers to call updateQuantity and removeItem with that
item id rather than productId. Keep the authenticated path unchanged and ensure
the unique guest id is used consistently wherever CartItemDTO is rendered or
acted on.

298-304: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Textos hardcodeados en español ("Estilo:", "Precio:", "Diseño subido") en un componente ya preparado para i18n.

El componente recibe labels para casi todos los textos (soldBy, remove, subtotal, checkout, etc.), pero estas tres cadenas se dejan fijas en español, rompiendo la traducción para el locale cat.

♻️ Mover a `labels`
   labels: {
     ...
     customizationColor: string;
+    unitPriceLabel: string;
+    uploadedDesignAlt: string;
     ...
   };
@@
                 {item.customization.color && (
                   <span className={styles.customizationLine}>
-                    Estilo: {item.customization.color}
+                    {labels.customizationColor}: {item.customization.color}
                   </span>
                 )}
@@
                   <img
                     src={item.customization.imageUrl}
-                    alt="Dise\u00f1o subido"
+                    alt={labels.uploadedDesignAlt}
                     className={styles.designThumb}
                   />
@@
                 <span className={styles.unitPrice}>
-                  <span className={styles.unitPriceLabel}>Precio:</span>{' '}
+                  <span className={styles.unitPriceLabel}>{labels.unitPriceLabel}</span>{' '}
                   {Money.format(item.unitPrice, Currency.EUR)}
                 </span>

Also applies to: 319-323, 352-356

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/cart/presentation/components/cart-view.tsx` around lines 298 - 304,
The cart view still has hardcoded Spanish strings for the customization section,
which breaks i18n for other locales. Update the affected text in CartView to
read from the existing labels prop instead of inline literals, using the same
pattern already used for soldBy, remove, subtotal, and checkout. Check the
customization rendering in cart-view.tsx (including the DesignThumb/image block
and the related style/price labels) and replace the fixed strings with label
keys so the locale-specific copy comes from labels.
shared/ui/file-upload-dropzone.tsx (1)

1-165: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Componente duplicado: ya existe shared/presentation/components/file-upload-dropzone.tsx con la misma interfaz y lógica.

FileUploadDropzoneItem, FileUploadDropzoneProps y la función handleFiles (líneas 7-29 y 57-67 del otro archivo) son prácticamente idénticos a este componente. Mantener dos implementaciones del mismo dropzone genérico en rutas distintas (shared/ui vs shared/presentation/components) genera divergencia de comportamiento a futuro y confusión sobre cuál es la versión canónica a importar.

Consolida en un único componente compartido y actualiza los imports de los consumidores (p. ej. ProductPhotoGallery, PhotoUploadField).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/ui/file-upload-dropzone.tsx` around lines 1 - 165, This dropzone is
duplicated in another shared component, so consolidate the implementation into a
single canonical FileUploadDropzone and remove the extra copy. Move or reuse the
shared symbols FileUploadDropzoneItem, FileUploadDropzoneProps, handleFiles, and
FileUploadDropzone from the shared location, then update all consumers such as
ProductPhotoGallery and PhotoUploadField to import the same component path.

@henri318
henri318 merged commit c2f503c into main Jul 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant