feat(products): add designer customizable products#105
Conversation
…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
|
Warning Review limit reached
Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughChangesPersonalización de producto end-to-end
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
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winLa sección de personalización renderiza un
<span>vacío cuando solo hayimageUrl.Se añadió
item.customization.imageUrla la condición de visibilidad (Líneas 269-270), pero el contenido del<span>solo concatenasize,colorytext. Si únicamente está presenteimageUrl, la condición es verdadera perofilter(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 elimageUrlno 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 winEvita crear personalizaciones antes de aplicar
keep-server.
Enkeep-servercon un carrito existente,resolveGuestCustomizationIdspuede persistir nuevas personalizaciones y luegoavailableGuestse 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 winDuplicación del mapeo de
labelsentre las páginas de crear y editar.Este bloque de
labelses prácticamente idéntico al deapp/[locale]/seller/products/[id]/edit/page.tsx(salvotitle/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 valueAserciones frágiles basadas en coincidencia de texto del código fuente.
Comprobar
not.toContain('Promise.all(')y la presencia literal defor (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, unPromise.allintroducido 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 valueEtiqueta del botón redundante durante la carga.
submitLabeles igual alabels.save, por lo queloading ? labels.save : submitLabelmuestra el mismo texto en ambos estados. Conviene usar una etiqueta tipo "Guardando…" mientrasloadingestruepara 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 valueEl log
customizablereflejapreviewEnabled, no la capacidad de personalización.
previewEnabledindica si hay previsualización, no si el producto es personalizable. Un producto conmode: 'description'(como la sudadera) es personalizable pero se loguea comocustomizable: no. Es solo un mensaje de seed, pero induce a confusión; considera derivarlo decustomizationConfig.mode !== 'description'o renombrar la etiqueta apreview.🤖 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 winSustituir
z.nativeEnumporz.enum
ProductStatuses un enum nativo de TypeScript y Zod 4 ya lo admite enz.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 winDuplicación exacta de
createCustomizationSchema.
createCustomerCustomizationSchema(líneas 34-38) es idéntico byte a byte acreateCustomizationSchema(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 valueValidación duplicada en ambas ramas.
El bloque
normalizeCustomizationDraft+customizationDraftSchema.safeParsecon 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 delif (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 valueRama inalcanzable en
getConfig.
assertAllowedsolo invocagetConfigcondto.productId, que aquí siempre coincide conproduct.id. La ramaproductId !== 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 winLecturas de producto secuenciales y posiblemente duplicadas.
Se construyen dos mapas (
capabilityProductMapen 77-82 yproductMapen 117-120) recorriendo IDs conawait findByIduno a uno (N+1), y ambos pueden solicitar los mismos productos dos veces. Si el repositorio ofrece unfindManyByIds/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 winEvita invocar
buildCustomizationHreftres veces por ítem.En el render se llama a
buildCustomizationHrefen la condición y dos veces más para elhref(Líneas 285, 292), recreandoURLSearchParamsen 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 winFalta 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
rateLimitMocka devolver{ blocked: true, reason: 'ip', retryAfterSeconds: 60 }y verifique el status 429, el headerRetry-Aftery querecordLoginAttemptno 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
recordLoginAttemptreutilizado para uploads acopla conceptos no relacionados.Registrar una "subida exitosa de invitado" mediante un método llamado
recordLoginAttemptconfunde 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
⛔ Files ignored due to path filters (3)
devresources/products/taza.pngis excluded by!**/*.pngpublic/img/products/customizable-hoodie.svgis excluded by!**/*.svgpublic/img/products/customizable-tshirt.svgis excluded by!**/*.svg
📒 Files selected for processing (79)
.gitignoreAGENTS.mdapp/[locale]/cart/cart-view.tsxapp/[locale]/cart/page.tsxapp/[locale]/checkout/page.tsxapp/[locale]/products/[id]/customization-draft-context.tsxapp/[locale]/products/[id]/customization-experience.tsxapp/[locale]/products/[id]/customization-form.tsxapp/[locale]/products/[id]/customization-preview.tsxapp/[locale]/products/[id]/page.module.cssapp/[locale]/products/[id]/page.tsxapp/[locale]/products/[id]/photo-upload-field.tsxapp/[locale]/products/[id]/preview-overlay.module.cssapp/[locale]/seller/products/[id]/edit/page.tsxapp/[locale]/seller/products/new/page.tsxapp/[locale]/seller/products/page.module.cssapp/[locale]/seller/products/page.tsxapp/[locale]/seller/products/product-form.tsxapp/api/cart/migrate/route.tsapp/api/cart/route.tsapp/api/customizations/customer/route.tsapp/api/products/[id]/route.tsapp/api/products/route.tsapp/api/uploads/guest/presigned-url/route.tscomponents/cart/add-to-cart-button.tsxcomponents/cart/customization-draft-schema.tsdocker-compose.e2e.ymldocker-compose.ymlmodules/cart/application/migrate-guest-cart.tsmodules/cart/domain/customization-create-port.tsmodules/cart/presentation/guest-cart-context.tsxmodules/cart/presentation/schemas/cart-schemas.tsmodules/customizations/application/create-customer-customization.tsmodules/customizations/presentation/schemas/customization-schemas.tsmodules/orders/application/handle-cart-checked-out.tsmodules/orders/infrastructure/product-repository-adapter.tsmodules/products/application/create-product-use-case.tsmodules/products/application/update-product-use-case.tsmodules/products/domain/entities/product.tsmodules/products/domain/product-capability-port.tsmodules/products/domain/value-objects/product-customization-config.tsmodules/products/infrastructure/mapper.tsmodules/products/infrastructure/prisma-product-repository.tsmodules/products/presentation/product-response.tsmodules/products/presentation/schemas/product-form-schema.tsmodules/uploads/presentation/schemas/upload-schemas.tsnext.config.tspackage.jsonprisma/migrations/20260629160000_add_product_customization_config_and_upload_type/migration.sqlprisma/schema.prismaprisma/seed-data.tsprisma/seed.tsshared/i18n/locales/cat.jsonshared/i18n/locales/es.jsonshared/infrastructure/prisma.tstests/e2e/products/products.spec.tstests/unit/app/[locale]/cart/cart-view-customization.test.tsxtests/unit/app/[locale]/products/[id]/customization-experience.test.tsxtests/unit/app/[locale]/products/[id]/customization-form.test.tsxtests/unit/app/[locale]/products/[id]/customization-preview.test.tsxtests/unit/app/[locale]/products/[id]/photo-upload-field-status.test.tsxtests/unit/app/[locale]/products/[id]/photo-upload-field.test.tsxtests/unit/app/[locale]/seller/products/page.test.tsxtests/unit/app/[locale]/seller/products/product-form.test.tsxtests/unit/app/api/cart/migrate/route.test.tstests/unit/app/api/customizations/customer/route.test.tstests/unit/app/api/products/[id]/route.test.tstests/unit/app/api/products/route.test.tstests/unit/app/api/uploads/guest/presigned-url/route.test.tstests/unit/components/cart/add-to-cart-button.test.tsxtests/unit/modules/cart/application/migrate-guest-cart.test.tstests/unit/modules/customizations/application/create-customer-customization.test.tstests/unit/modules/products/application/create-product-use-case.test.tstests/unit/modules/products/application/update-product-use-case.test.tstests/unit/modules/products/domain/value-objects/product-customization-config.test.tstests/unit/modules/products/infrastructure/prisma-product-repository.test.tstests/unit/modules/products/infrastructure/product-customization-config-mapper.test.tstests/unit/prisma/prisma-seed-config.test.tstests/unit/prisma/seed-data.test.ts
| if ( | ||
| imageUrl && | ||
| !/^https?:\/\//.test(imageUrl) && | ||
| !imageUrl.startsWith('/') | ||
| ) { | ||
| nextErrors.imageUrl = 'Customization image must be a valid URL.'; | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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; |
There was a problem hiding this comment.
🗄️ 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.
| { | ||
| protocol: 'https', | ||
| hostname: '**', | ||
| }, |
There was a problem hiding this comment.
🔒 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
fiRepository: 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:
- 1: https://justappsec.com/guides/nextjs-ssrf-protection
- 2: Allowing all remotePatterns when using next/image - is it safe? vercel/next.js#71843
- 3: Best way to handle Image with arbitrary src vercel/next.js#46399
- 4: https://www.tenable.com/plugins/was/114430
- 5: https://www.invicti.com/web-application-vulnerabilities/next-js-image-blind-ssrf
- 6: https://nextjs.org/docs/14/app/building-your-application/optimizing/images
- 7: https://nextjs.org/docs/pages/api-reference/components/image
- 8: https://nextjs.org/docs/messages/next-image-unconfigured-host
- 9: https://dev.to/joodi/allow-all-domains-for-images-in-nextjs-hpj
🏁 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.tsRepository: 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:
- 1: https://nextjs.org/docs/13/pages/api-reference/components/image
- 2: https://nextjs.org/docs/app/api-reference/components/image
- 3: https://nextjs.org/docs/api-reference/next/image
- 4: https://nextjs.org/docs/messages/next-image-unconfigured-host
- 5: https://nextjs.org/docs/15/app/getting-started/images
- 6: https://nextjs.org/docs/14/app/building-your-application/optimizing/images
- 7: https://nextjs.org/docs/app/getting-started/images
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.
| "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", |
There was a problem hiding this comment.
🩺 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:
- 1: Add timeout to --wait option docker/compose#10269
- 2: introduce up --wait condition docker/compose#8777
- 3: https://docs.docker.com/reference/cli/docker/compose/up/
- 4: https://manpages.debian.org/unstable/docker-compose/docker-compose-up.1.en.html
- 5: https://github.com/docker/compose/blob/e5c6b9c3/docs/reference/docker_compose_up.yaml
- 6: [BUG] compose up -d waits for service dependencies before detaching docker/compose#12812
- 7: https://docs.docker.com/compose/how-tos/startup-order/
- 8: https://www.howtogeek.com/devops/how-to-make-docker-compose-wait-for-dependency-containers/
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.
| 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(); |
There was a problem hiding this comment.
🎯 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' -C2Repository: 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
doneRepository: 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.tsRepository: 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.tsRepository: 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
doneRepository: 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
🎯 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.
| 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
…ucts_pr3 Feat/designer customizable products pr3
There was a problem hiding this comment.
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 winBug de integridad: se usa
translations[0]en vez de la traducción del locale objetivo.
currentTranslationse toma comoproduct.translations[0], no como la traducción existente paradto.locale. Si se actualiza un producto en un locale distinto al de la primera traducción (p. ej.dto.locale = 'cat'contranslations[0]en'es') sin enviarname/description,nextTranslationheredará elname/descriptionde 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 cambiarpriceostatus).🐛 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 liftUnifica la actualización y el evento en una sola transacción.
productRepository.update()abre su propia transacción ysaveEvent()usaprismapor defecto, así que aquí se ejecutan por separado; si falla el outbox, el producto ya queda confirmado sinPRODUCT_UPDATED. Pasa ambos cambios por el mismotxo encapsula esta lógica en untransactionRunner.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 winLos errores de validación en campos distintos a name/description/price/customizationConfig no se muestran.
FormErrorssolo define claves paraname,description,priceycustomizationConfig. SiproductFormSchema.safeParsedevuelve un issue conpath[0] === 'images'(p. ej. lista vacía o alt inválido no bloqueado por elrequirednativo),mapErrorslo guardaría igualmente en el objeto (JS no aplica el tipo en runtime), pero no hay ningúnerrors.imagesrenderizado 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.imagescerca deProductPhotoGallery, por ejemplo pasándolo como prop de error adicional o mostrándolo junto alphotoErrorexistente.🤖 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 winRegresión de rendimiento: resolución secuencial de productos.
El poblamiento de
productMappasó de resolverproductIdsen paralelo (Promise.all) a hacerlo secuencialmente conawaitdentro delfor...of. Esto convierte N consultas potencialmente paralelas en N consultas en cadena, aumentando la latencia deGET /api/cartde 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 winValidar
customizationIdListantes de persistirloEn
modules/cart/application/update-cart-item.ts:81-88, se guardadto.customizationIdListtal cual. Falta comprobar que esos IDs existan y pertenezcan alproductIddel ítem, como ya haceadd-item-to-cart.ts; si no, un usuario puede asociar personalizaciones ajenas y la ruta de lectura las resolverá y devolverá encustomizations.🤖 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 winMismo problema de estado del contexto 2D que en
app/[locale]/cart/design-preview.tsx.
draw()no reinicia la matriz de transformación niglobalAlpha/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 valueSelecció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"conaria-checkedcomunicarí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 valueInconsistencia de nomenclatura:
rotation_deg/blend_modeen 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 usarrotationDeg/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 valueRecalcula el borrador normalizado en vez de reutilizar
normalizedCustomization.
handleSaveDesignllama anormalizeCustomizationDraft(customization)de nuevo, ignorando el valor ya memoizadonormalizedCustomizationcalculado 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 winFalta cobertura de test para
designPositionen el matching de personalización.Estos tests validan campos de texto/color/tamaño/imagen pero no
designPosition. Dado el hallazgo enadd-to-cart-button.tsxdondeguestCustomizationMatches/authCustomizationMatchesignorandesignPosition, 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 winLógica de mapeo de
designPositionduplicada y tipadounknowninnecesario.El casteo de
designPositiondeunknowna la forma específica se repite de forma idéntica enapp/api/customizations/[id]/route.ts. Además, el parámetro se tipa comounknowncuando la entidad de dominio (CustomizationEntity.designPosition: DesignPositionValue | null) ya aporta el tipo correcto, por lo que elas {...}es un casteo innecesario que oculta información de tipos ya disponible.Sugiero extraer un serializador compartido dentro del módulo
customizations(p. ej. enmodules/customizations/presentation/) que recibaCustomizationEntitydirectamente, 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 winCorregir 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.formColen 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 | 🔵 TrivialDuplicación de constantes de validación entre capa de presentación y dominio.
designPositionInputSchemareproduce con números literales (0-1, 10-200, -45/45, 0-100) los mismos límites que ya están centralizados comoMIN_DESIGN_SCALE_PERCENT/MAX_DESIGN_SCALE_PERCENT/etc. enproduct-customization-config.ts(capa de dominio). Además,productCustomizationConfigSchemaaquí impone.max(2000)endesignChangeDescriptiony.max(20)entagNames, límites que el esquema equivalente del dominio no reproduce, dejando una vía de entrada sin ese tope si algo invocaProductCustomizationConfig.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 valueEvaluar si este script de depuración debe versionarse.
tmp/check-db.tsconsulta directamente todas las tablas (bypass de repositorios/módulos) y no tiene ninguna guarda de entorno (p. ej. bloquear ejecución siNODE_ENV=production). Considera moverlo a.gitignoreo 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 winDuplicación de
.fileInputy.visuallyHidden, y uso de la propiedad deprecadaclip.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 modernoclip-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
productIdes opcional incluso en modo'edit', sin garantía de tipo.Si en el futuro se omite
productIdconmode="edit",endpointresultaría en/api/products/undefinedsin error de compilación. Una unión discriminada pormodeevitarí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 winAñ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 winPropiedad
clipdeprecada; usarclip-path.
clipestá deprecada por CSS Masking Level 1. Para el patrón "visually hidden" del input, la alternativa moderna y ampliamente soportada esclip-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 valueNotació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 porcolor-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 valueExtraer la coerción de
designPositiona un helper compartido.
Esta lógica es idéntica amodules/orders/infrastructure/prisma-order-repository.ts; moverla a un helper enshared/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
setDesignPositionno limpiaerror/errorscomo el resto de setters.A diferencia de
setText,setColor,setSize,setImageyclearImage, este setter no reseteadraft.errorni llama aclearValidationErrors(), 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 tradeoffCasting 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 winArchivo 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 winLógica de personalización duplicada.
updateCustomizationyupdateItemCustomizationson prácticamente idénticas; solo difieren en la clave de coincidencia (productIdvsid). 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 porid).🤖 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 winEjecuta
findManyycounten paralelo.Al sustituir la transacción combinada por dos
awaitsecuenciales se añade una ida y vuelta adicional a la base de datos. Como no dependen entre sí, pueden lanzarse en paralelo conPromise.allpara 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 winRutas 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 losSKILL.mdindicados, 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 valueComentario 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 usaLocalStorageAdapterfuera 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 valueMezcla de convenciones de nombres (
rotation_deg,blend_modevs. camelCase).El resto del esquema (
imageUrl,customizationImageUploadId, etc.) usa camelCase, perorotation_degyblend_modeestá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_modereflejan 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 winEl evento
CART_ITEM_UPDATEDno incluyecustomizationIdList.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
⛔ Files ignored due to path filters (23)
devresources/products/azul.pngis excluded by!**/*.pngdevresources/products/paisaje-profundidad-lineas-734x489.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0424fd7c-41cf-48ee-8215-2a1f7636c1f9.pngis excluded by!**/*.pngtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0ba87ba4-8a04-478b-88a2-1ecc490dabbc.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/54b3f06b-2044-456a-9757-75505771f677.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/7072116f-5974-42f6-8344-23861596b58e.pngis excluded by!**/*.pngtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/86912d37-6fc1-4f24-8ff5-fae00ff1eb12.pngis excluded by!**/*.pngtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ade99071-3c02-4ed4-b640-f96aa3dd2db4.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ddac64c9-c822-4631-93f4-e1f1133caa04.jpgis excluded by!**/*.jpgtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/0770179d-365d-4b8a-b278-453d1e6b717e.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/51d82e86-9260-4eab-9fac-77513ad454cd.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/72e1a4df-88ce-4d82-ae8e-300a65f8c76d.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/7748b1c2-6b6d-4dc2-b3d5-f8717a581d6c.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/859fae6e-2c78-46ee-b025-1fd8048e87b5.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ed250a15-8870-40f6-a1ec-8a8845ada3ed.pngis excluded by!**/*.pngtmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ef719e39-2897-4106-ad16-a2339739f06a.pngis excluded by!**/*.pngtmp/uploads/product/cmr23m83f0001iotkjny1yugb/34162f3a-0a1e-4c92-8088-99db5e0cd3e9.pngis excluded by!**/*.pngtmp/uploads/product/cmr23m83f0001iotkjny1yugb/c9abdded-5e1b-4697-b236-d3380155bb5c.pngis excluded by!**/*.pngtmp/uploads/product/cmr23m83f0001iotkjny1yugb/cfe23339-3140-4855-99d0-cbf16d3b8412.pngis excluded by!**/*.pngtmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/530f7dac-9b5f-4a14-adc3-717a7f603532.pngis excluded by!**/*.pngtmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/9026017f-9c52-4e5b-b6be-2c8e8649caf6.jpgis excluded by!**/*.jpgtmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/94a8b1bb-3778-4cfa-80f2-38e2778cf1c2.pngis excluded by!**/*.pngtmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/a8dfbda4-a431-406d-835e-9bd18e9f7eb2.pngis excluded by!**/*.png
📒 Files selected for processing (121)
.atl/.skill-registry.cache.json.atl/skill-registry.mdapp/[locale]/cart/cart-view.module.cssapp/[locale]/cart/cart-view.tsxapp/[locale]/cart/design-preview.tsxapp/[locale]/cart/page.tsxapp/[locale]/checkout/page.tsxapp/[locale]/page.module.cssapp/[locale]/page.tsxapp/[locale]/products/[id]/customization-draft-context.tsxapp/[locale]/products/[id]/customization-experience.module.cssapp/[locale]/products/[id]/customization-experience.tsxapp/[locale]/products/[id]/customization-form.module.cssapp/[locale]/products/[id]/customization-form.tsxapp/[locale]/products/[id]/customization-preview.tsxapp/[locale]/products/[id]/mockup-canvas-control.module.cssapp/[locale]/products/[id]/mockup-canvas-control.tsxapp/[locale]/products/[id]/page.module.cssapp/[locale]/products/[id]/page.tsxapp/[locale]/products/[id]/photo-upload-field.tsxapp/[locale]/products/[id]/preview-overlay.module.cssapp/[locale]/seller/products/[id]/edit/page.tsxapp/[locale]/seller/products/new/page.tsxapp/[locale]/seller/products/page.module.cssapp/[locale]/seller/products/page.tsxapp/[locale]/seller/products/product-form.module.cssapp/[locale]/seller/products/product-form.tsxapp/[locale]/seller/products/product-photo-gallery.tsxapp/api/cart/items/[itemId]/route.tsapp/api/cart/items/route.tsapp/api/cart/migrate/route.tsapp/api/cart/route.tsapp/api/customizations/[id]/route.tsapp/api/customizations/customer/route.tsapp/api/customizations/route.tsapp/api/products/[id]/route.tsapp/api/products/route.tsapp/api/uploads/local/[...storageKey]/route.tscomponents/cart/add-to-cart-button.module.csscomponents/cart/add-to-cart-button.tsxcomponents/cart/add-to-cart-choice-modal.module.csscomponents/cart/add-to-cart-choice-modal.tsxcomponents/cart/customization-draft-schema.tscomposition-root/container.tsdevresources/products/example.webpmodules/cart/application/migrate-guest-cart.tsmodules/cart/application/update-cart-item.tsmodules/cart/domain/customization-create-port.tsmodules/cart/domain/customization-lookup-port.tsmodules/cart/infrastructure/customization-lookup-adapter.tsmodules/cart/presentation/guest-cart-context.tsxmodules/cart/presentation/schemas/cart-schemas.tsmodules/customizations/application/__tests__/customization-queries.test.tsmodules/customizations/application/__tests__/customization-use-cases.test.tsmodules/customizations/application/create-customer-customization.tsmodules/customizations/application/create-customization.tsmodules/customizations/application/update-customization.tsmodules/customizations/domain/__tests__/customization-domain.test.tsmodules/customizations/domain/entities/customization.tsmodules/customizations/domain/value-objects/customization-options.tsmodules/customizations/infrastructure/prisma-customization-repository.tsmodules/customizations/presentation/schemas/customization-schemas.tsmodules/orders/application/handle-cart-checked-out.tsmodules/orders/domain/customization-lookup-port.tsmodules/orders/infrastructure/prisma-order-repository.tsmodules/presentation/components/cart-popup.module.cssmodules/presentation/components/cart-popup.tsxmodules/presentation/components/design-preview.tsxmodules/products/application/create-product-use-case.tsmodules/products/application/update-product-use-case.tsmodules/products/domain/value-objects/product-customization-config.tsmodules/products/infrastructure/mapper.tsmodules/products/infrastructure/prisma-product-repository.tsmodules/products/presentation/components/product-customization-config-editor.module.cssmodules/products/presentation/components/product-customization-config-editor.tsxmodules/products/presentation/schemas/product-form-schema.tsmodules/sellers/infrastructure/prisma-seller-repository.tsmodules/uploads/application/create-upload-use-case.tsmodules/uploads/infrastructure/local-storage-adapter.tsnext.config.tsprisma/migrations/20260701100000_add_customization_design_position/migration.sqlprisma/schema.prismaprisma/seed-data.tssdd/customizations-module/exploration.mdsdd/explore-shared-refactor/exploration.mdsdd/hexagonal-cleanup/tasks.mdsdd/seller-module/design.mdshared/i18n/locales/cat.jsonshared/i18n/locales/es.jsonshared/presentation/components/file-upload-dropzone.module.cssshared/presentation/components/file-upload-dropzone.tsxshared/presentation/lib/to-absolute-url.tstests/doubles/memory-customization-lookup.tstests/doubles/memory-order-customization-lookup.tstests/integration/customizations/prisma-customization-repository.integration.test.tstests/integration/orders/handle-cart-checked-out.integration.test.tstests/integration/orders/prisma-order-repository.integration.test.tstests/unit/app/[locale]/page.test.tsxtests/unit/app/[locale]/products/[id]/customization-experience.test.tsxtests/unit/app/[locale]/products/[id]/customization-form.test.tsxtests/unit/app/[locale]/products/[id]/customization-preview.test.tsxtests/unit/app/[locale]/products/[id]/page.test.tsxtests/unit/app/[locale]/products/[id]/photo-upload-field-status.test.tsxtests/unit/app/[locale]/products/[id]/photo-upload-field.test.tsxtests/unit/app/[locale]/seller/products/product-form.test.tsxtests/unit/app/api/uploads/local/[...storageKey]/route.test.tstests/unit/components/cart/add-to-cart-button.test.tsxtests/unit/components/cart/guest-cart-badge.test.tsxtests/unit/modules/cart/application/migrate-guest-cart.test.tstests/unit/modules/orders/application/handle-cart-checked-out.test.tstests/unit/modules/presentation/components/cart-icon.test.tsxtests/unit/modules/products/application/create-product-use-case.test.tstests/unit/modules/products/application/update-product-use-case.test.tstests/unit/modules/products/domain/value-objects/product-customization-config.test.tstests/unit/modules/products/infrastructure/prisma-product-repository.test.tstests/unit/modules/products/infrastructure/product-customization-config-mapper.test.tstests/unit/modules/uploads/application/create-upload-use-case.test.tstests/unit/modules/uploads/infrastructure/local-storage-adapter.test.tstests/unit/shared/presentation/components/file-upload-dropzone.test.tsxtmp/check-db.tstmp/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
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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)); |
There was a problem hiding this comment.
📐 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
| await fetch(result.uploadUrl, { | ||
| method: 'PUT', | ||
| headers: { 'content-type': file.type }, | ||
| body: file, | ||
| }); | ||
|
|
||
| const imageUrl = toAbsoluteUrl(result.publicUrl); | ||
| setImage({ imageUploadId: result.id, imageUrl }); |
There was a problem hiding this comment.
🩺 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.
| 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.
| .colorItem:hover { | ||
| border-color: rgba(0, 0, 0, 0.2); | ||
| } |
There was a problem hiding this comment.
📐 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.
| .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
| const textErrorId = errors.text ? 'customization-text-error' : undefined; | ||
| const _colorErrorId = errors.color ? 'customization-color-error' : undefined; | ||
| const sizeErrorId = errors.size ? 'customization-size-error' : undefined; |
There was a problem hiding this comment.
🎯 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.
| customization: { | ||
| text: item.customizationText ?? null, | ||
| color: item.customizationColor ?? null, | ||
| size: item.customizationSize ?? null, | ||
| imageUrl: item.customizationImageUrl ?? null, | ||
| colorImageUrl: item.productImageUrl ?? null, | ||
| }, |
There was a problem hiding this comment.
🎯 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.
| 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`.
| 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(); |
There was a problem hiding this comment.
🗄️ 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
| <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> |
There was a problem hiding this comment.
🎯 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.
| aria-describedby="tags-help" | ||
| /> | ||
| <span id="tags-help" className={styles.help}> | ||
| {labels.tagsHelp} | ||
| </span> |
There was a problem hiding this comment.
📐 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.
| 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.
| const users = await prisma.user.findMany(); | ||
| console.log('--- Users ---'); | ||
| console.log(users.map((u) => ({ id: u.id, email: u.email, role: u.role }))); |
There was a problem hiding this comment.
🔒 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.
| 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
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (7)
app/[locale]/seller/products/page.tsx (1)
140-145: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsidera
next/linkpara 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 connext/linkpara 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 conLink.🤖 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 winAviso de Stylelint: nombres de clase no kebab-case.
Stylelint marca
.checkoutBtncomo no kebab-case; el mismo patrón camelCase se repite en todo el archivo (.cta,.itemInfo,.fullCartBtn) y también encart-view.module.css(.ctaButton,.itemDetails). Si el proyecto usa CSS Modules con camelCase intencionalmente para el accesostyles.xxxen JS, sería más adecuado ajustar la reglaselector-class-patternde 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
buildCustomizationHrefse 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 winDoble cast
as unknown as DesignPositionDatasin validación de forma.
customization.designPositionse tipa comoRecord<string, unknown> | nully luego se fuerza aDesignPositionDatavía doble cast antes de pasarlo aDesignPreview, que usa directamentedesignPosition.blend_mode/.opacitypara el canvas. Si el payload de/api/cartno coincide exactamente con la forma esperada, esto puede degradar silenciosamente el render (p. ej.globalAlphaenNaN) sin ningún aviso en tiempo de compilación ni runtime.cart-view.tsxya definecustomization.designPositioncon la forma concreta (imageUrl, x, y, scale, rotation_deg, opacity, blend_mode) en vez deRecord<string, unknown>; alinear este archivo con ese tipo (o con el esquema Zod deCustomizationOptionsdel 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 winFalta cobertura para el matching por
designPosition.Dado el bug señalado en
add-to-cart-button.tsx(el matching ignoradesignPosition), sería valioso añadir un caso que verifique que dos personalizaciones con el mismo texto/imagen pero distintadesignPositionse 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 winNotació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 modernargb(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 winConsolida
components/cart/customization-draft-schema.tsycomponents/cart/add-to-cart-choice-modal.tsx.
Ahora duplican exactamentemodules/cart/presentation/components/*; conviértelos en re-exports o elimínalos para evitar divergencias futuras.components/cart/add-to-cart-button.tsxya 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
⛔ Files ignored due to path filters (4)
tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/43150e8a-f92c-4d99-b36f-9ad8b12c92fd.pngis excluded by!**/*.pngtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/8299071a-2631-4a3e-bc8f-cdba90a76c3d.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/c2421aa8-a506-4e1e-b738-e47e64d8956c.jpgis excluded by!**/*.jpgtmp/uploads/product/cmr4su2n40001hstky222wdt1/0b154df8-be9c-4f79-a72c-08c31981c3d1.pngis excluded by!**/*.png
📒 Files selected for processing (34)
.gitignoreAGENTS.mdapp/[locale]/cart/cart-view.tsxapp/[locale]/checkout/page.tsxapp/[locale]/products/[id]/customization-experience.tsxapp/[locale]/seller/products/page.module.cssapp/[locale]/seller/products/page.tsxcomponents/cart/add-to-cart-button.tsxcomponents/cart/add-to-cart-choice-modal.tsxmodules/cart/presentation/components/add-to-cart-button.module.cssmodules/cart/presentation/components/add-to-cart-button.tsxmodules/cart/presentation/components/add-to-cart-choice-modal.module.cssmodules/cart/presentation/components/add-to-cart-choice-modal.tsxmodules/cart/presentation/components/cart-popup.module.cssmodules/cart/presentation/components/cart-popup.tsxmodules/cart/presentation/components/cart-view.module.cssmodules/cart/presentation/components/cart-view.tsxmodules/cart/presentation/components/customization-draft-schema.tsmodules/products/infrastructure/prisma-product-repository.tsprisma/schema.prismaprisma/seed.tsshared/i18n/locales/cat.jsonshared/i18n/locales/es.jsonshared/ui/file-upload-dropzone.module.cssshared/ui/file-upload-dropzone.tsxtests/e2e/products/products.spec.tstests/unit/app/[locale]/cart/cart-view-customization.test.tsxtests/unit/app/[locale]/page.test.tsxtests/unit/app/[locale]/products/[id]/customization-experience.test.tsxtests/unit/app/[locale]/products/[id]/page.test.tsxtests/unit/app/[locale]/seller/products/page.test.tsxtests/unit/app/cart/cart-view.test.tsxtests/unit/components/cart/add-to-cart-button.test.tsxtests/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
There was a problem hiding this comment.
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 valueConsidera
next/linkpara 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 connext/linkpara 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 conLink.🤖 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 winAviso de Stylelint: nombres de clase no kebab-case.
Stylelint marca
.checkoutBtncomo no kebab-case; el mismo patrón camelCase se repite en todo el archivo (.cta,.itemInfo,.fullCartBtn) y también encart-view.module.css(.ctaButton,.itemDetails). Si el proyecto usa CSS Modules con camelCase intencionalmente para el accesostyles.xxxen JS, sería más adecuado ajustar la reglaselector-class-patternde 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
buildCustomizationHrefse 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 winDoble cast
as unknown as DesignPositionDatasin validación de forma.
customization.designPositionse tipa comoRecord<string, unknown> | nully luego se fuerza aDesignPositionDatavía doble cast antes de pasarlo aDesignPreview, que usa directamentedesignPosition.blend_mode/.opacitypara el canvas. Si el payload de/api/cartno coincide exactamente con la forma esperada, esto puede degradar silenciosamente el render (p. ej.globalAlphaenNaN) sin ningún aviso en tiempo de compilación ni runtime.cart-view.tsxya definecustomization.designPositioncon la forma concreta (imageUrl, x, y, scale, rotation_deg, opacity, blend_mode) en vez deRecord<string, unknown>; alinear este archivo con ese tipo (o con el esquema Zod deCustomizationOptionsdel 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 winFalta cobertura para el matching por
designPosition.Dado el bug señalado en
add-to-cart-button.tsx(el matching ignoradesignPosition), sería valioso añadir un caso que verifique que dos personalizaciones con el mismo texto/imagen pero distintadesignPositionse 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 winNotació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 modernargb(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 winConsolida
components/cart/customization-draft-schema.tsycomponents/cart/add-to-cart-choice-modal.tsx.
Ahora duplican exactamentemodules/cart/presentation/components/*; conviértelos en re-exports o elimínalos para evitar divergencias futuras.components/cart/add-to-cart-button.tsxya 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
⛔ Files ignored due to path filters (4)
tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/43150e8a-f92c-4d99-b36f-9ad8b12c92fd.pngis excluded by!**/*.pngtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/8299071a-2631-4a3e-bc8f-cdba90a76c3d.jpgis excluded by!**/*.jpgtmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/c2421aa8-a506-4e1e-b738-e47e64d8956c.jpgis excluded by!**/*.jpgtmp/uploads/product/cmr4su2n40001hstky222wdt1/0b154df8-be9c-4f79-a72c-08c31981c3d1.pngis excluded by!**/*.png
📒 Files selected for processing (34)
.gitignoreAGENTS.mdapp/[locale]/cart/cart-view.tsxapp/[locale]/checkout/page.tsxapp/[locale]/products/[id]/customization-experience.tsxapp/[locale]/seller/products/page.module.cssapp/[locale]/seller/products/page.tsxcomponents/cart/add-to-cart-button.tsxcomponents/cart/add-to-cart-choice-modal.tsxmodules/cart/presentation/components/add-to-cart-button.module.cssmodules/cart/presentation/components/add-to-cart-button.tsxmodules/cart/presentation/components/add-to-cart-choice-modal.module.cssmodules/cart/presentation/components/add-to-cart-choice-modal.tsxmodules/cart/presentation/components/cart-popup.module.cssmodules/cart/presentation/components/cart-popup.tsxmodules/cart/presentation/components/cart-view.module.cssmodules/cart/presentation/components/cart-view.tsxmodules/cart/presentation/components/customization-draft-schema.tsmodules/products/infrastructure/prisma-product-repository.tsprisma/schema.prismaprisma/seed.tsshared/i18n/locales/cat.jsonshared/i18n/locales/es.jsonshared/ui/file-upload-dropzone.module.cssshared/ui/file-upload-dropzone.tsxtests/e2e/products/products.spec.tstests/unit/app/[locale]/cart/cart-view-customization.test.tsxtests/unit/app/[locale]/page.test.tsxtests/unit/app/[locale]/products/[id]/customization-experience.test.tsxtests/unit/app/[locale]/products/[id]/page.test.tsxtests/unit/app/[locale]/seller/products/page.test.tsxtests/unit/app/cart/cart-view.test.tsxtests/unit/components/cart/add-to-cart-button.test.tsxtests/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.
guestCustomizationMatchesyauthCustomizationMatchescomparantext,color,sizeeimageUrl, pero ignorandesignPositionen la comparación real (aunque sí se usa para decidir si el borrador "tiene contenido" enauthCustomizationMatches/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.tsxRepository: 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 -nRepository: 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 porproductId.
guestCart.itemsya puede contener varias entradas del mismo producto con ids distintos. Aquí se sigue mapeandoid: gi.productIdy se llamanupdateQuantity(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
labelspara 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 localecat.♻️ 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.tsxcon la misma interfaz y lógica.
FileUploadDropzoneItem,FileUploadDropzonePropsy la funciónhandleFiles(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/uivsshared/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.
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
Bug Fixes