Skip to content

Chore/clean unused files create common components#123

Merged
henri318 merged 6 commits into
mainfrom
chore/clean_unused_files_create_common_components
Jul 7, 2026
Merged

Chore/clean unused files create common components#123
henri318 merged 6 commits into
mainfrom
chore/clean_unused_files_create_common_components

Conversation

@henri318

@henri318 henri318 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Se mejoró la experiencia de carritos, pedidos, productos y vendedores con componentes y páginas más consistentes.
    • Se añadieron utilidades para formularios, paginación, validación y previsualización de diseños.
  • Bug Fixes
    • Se unificó la gestión de sesión y validación en varios flujos para hacerlos más fiables.
    • Se normalizó el manejo de personalizaciones, estados de pedidos y artículos del carrito.
  • Chores
    • Se reorganizaron estilos, rutas y dependencias internas para simplificar el mantenimiento.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 6 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c1a74ce-6863-46cd-9429-29cfca397155

📥 Commits

Reviewing files that changed from the base of the PR and between 9e868ca and 3957fce.

📒 Files selected for processing (8)
  • .gitignore
  • app/[locale]/orders/page.tsx
  • app/[locale]/seller/orders/page.tsx
  • modules/cart/presentation/components/cart-view.tsx
  • modules/orders/presentation/order-page-url.ts
  • modules/orders/presentation/schemas/order-schemas.ts
  • shared/ui/input.tsx
  • shared/ui/text-field.tsx
📝 Walkthrough

Walkthrough

Este PR configura jscpd, extrae numerosas utilidades compartidas (validación de formularios, sesión, canvas, paginación, DTOs de carrito, enriquecimiento de items, columnas de tabla, respuestas API), y refactoriza páginas y rutas API para consumirlas, eliminando implementaciones duplicadas y archivos obsoletos (design-preview, EntityId, OutboxService antiguo, BrevoEmailSender antiguo, add-to-cart-choice-modal, file-upload-dropzone).

Changes

Deduplicación y refactor de utilidades compartidas

Layer / File(s) Summary
Configuración de jscpd y reporte
.jscpd.json, .gitignore, report/jscpd-report.html
Se configura jscpd con umbral, reporters y patrones ignorados, se ajusta .gitignore y se genera el reporte HTML de duplicación.
Validación de formularios compartida
shared/validation/validate-form.ts, shared/validation/name-validator.ts, shared/presentation/use-form-field.ts, app/[locale]/admin/sellers/create/page.tsx, app/[locale]/auth/signup/page.tsx, modules/users/application/use-cases/*
Se añaden validateForm, validateName y el hook useFormField, aplicados en formularios de creación de seller, signup y casos de uso de usuarios.
Contexto de sesión centralizado
shared/authorization/session-user-context.ts, app/api/sellers/[id]/route.ts, app/api/uploads/[id]/route.ts
getSessionUserContext reemplaza getServerSession(authOptions) en las rutas de sellers y uploads.
Route helpers y refactor de rutas de carrito/personalizaciones
shared/presentation/route-helpers.ts, modules/cart/presentation/enrich-cart-item.ts, modules/customizations/presentation/to-customization-response.ts, modules/sellers/presentation/seller-response.ts, app/api/cart/*, app/api/customizations/*, app/api/sellers/route.ts
Se centralizan autenticación, extracción de params, parsing de body y transformación de respuestas para rutas de carrito, personalizaciones y sellers.
Validación de acceso a carrito
modules/cart/application/cart-access.ts, modules/cart/application/remove-cart-item.ts, modules/cart/application/update-cart-item.ts
loadAndVerifyCart centraliza la carga y validación de propiedad/estado del carrito, usada por los casos de uso de actualizar/eliminar ítems.
Value object DesignPosition y esquema compartido
shared/kernel/domain/value-objects/design-position.ts, shared/validation/design-position-schema.ts, modules/customizations/*, modules/orders/infrastructure/prisma-order-repository.ts, modules/cart/infrastructure/customization-lookup-adapter.ts, modules/cart/presentation/components/customization-draft-schema.ts
coerceDesignPosition y designPositionSchema reemplazan implementaciones locales duplicadas en repositorios y esquemas.
DTOs y eventos de carrito compartidos
modules/cart/presentation/cart-dto.ts, modules/cart/presentation/cart-events.ts, modules/cart/presentation/components/*, modules/cart/presentation/guest-cart-context.tsx
CartItemDTO, guestItemToDTO, CART_UPDATED_EVENT y dispatchCartUpdated reemplazan definiciones locales; se añaden helpers de matching de personalización y normalización de guest cart.
Eliminación de código duplicado
shared/presentation/canvas-utils.ts, app/[locale]/products/[id]/mockup-canvas-control.tsx, modules/presentation/components/design-preview.tsx, app/[locale]/cart/design-preview.tsx, components/cart/add-to-cart-choice-modal.*, components/cart/customization-draft-schema.ts, shared/presentation/components/file-upload-dropzone.*, shared/kernel/domain/identifiers/*
Se extraen loadImage/drawCover/drawDesign a canvas-utils y se eliminan archivos y clases duplicadas (design-preview de cart, modal de elección, dropzone en presentation, EntityId).
Columnas de tabla y etiquetas compartidas
modules/products/presentation/*, modules/orders/presentation/*, app/[locale]/admin/sellers/*, app/[locale]/seller/*, app/[locale]/orders/page.tsx
createProductTableColumns, createOrderCommonColumns, getProductFormLabels, buildOrderPageUrl y computePaginationState centralizan la construcción de tablas y paginación en múltiples páginas.
Refactor de Input/TextField
shared/ui/input.tsx, shared/ui/text-field.tsx
Input acepta classNames para sobrescribir estilos y TextField delega completamente en Input.
Reubicación de OutboxService y BrevoEmailSender
shared/infrastructure/outbox-service.ts, modules/email/infrastructure/brevo-email-sender.ts, composition-root/container.ts, workers/outbox-worker.ts, tests/unit/*
Se eliminan las implementaciones previas y se actualizan imports para apuntar a shared/kernel/*.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Route as API Route Carrito
  participant Helpers as route-helpers
  participant UseCase
  participant Enricher as enrichCartItem

  Route->>Helpers: getAuthenticatedUserId()
  Helpers-->>Route: userId o null (401)
  Route->>Helpers: parseBody(request, schema)
  Helpers-->>Route: body validado
  Route->>UseCase: execute(dto)
  UseCase-->>Route: item/cart
  Route->>Enricher: enrichCartItem(item, product, customizations)
  Enricher-->>Route: DTO enriquecido
  Route-->>Route: respuesta JSON
Loading
sequenceDiagram
  participant Componente as Componente de Carrito
  participant Events as cart-events
  participant CartIcon

  Componente->>Events: dispatchCartUpdated()
  Events->>CartIcon: dispatchEvent(cart:updated)
  CartIcon->>CartIcon: refresca contador
Loading

Possibly related PRs

  • henri318/728-store#1: Refactoriza el mismo patrón de outbox, moviendo OutboxService de shared/infrastructure a shared/kernel.
  • henri318/728-store#76: Refactoriza los mismos endpoints de carrito (/api/cart/*) para centralizar auth y parsing de body.
  • henri318/728-store#96: Modifica la misma página de productos de admin para reconstruir la tabla con columnas centralizadas.

Suggested labels: type:chore

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed El título refleja el refactor principal: limpieza de archivos innecesarios y creación de componentes/utilidades comunes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/clean_unused_files_create_common_components

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
app/[locale]/admin/sellers/create/page.tsx (1)

102-130: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

normalizePayload(form) se llama dos veces innecesariamente.

payload se calcula en la línea 116 (para validar con validateFormGeneric) y se vuelve a calcular en la línea 130 dentro del try, sombreando la variable anterior con el mismo resultado. Esto es cómputo redundante y una fuente potencial de confusión/drift si en el futuro se modifica solo una de las dos invocaciones.

🔧 Fix propuesto: reutilizar el payload ya calculado
     setLoading(true);
 
     try {
-      const payload = normalizePayload(form);
       const res = await fetch('/api/sellers', {
🤖 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]/admin/sellers/create/page.tsx around lines 102 - 130, The
submit handler in handleSubmit recalculates normalizePayload(form) twice, once
before validateFormGeneric and again inside the try block, which is redundant
and shadows the existing payload variable. Reuse the already computed payload
for both validation and the subsequent submit logic, and remove the second
normalizePayload(form) call so the data path stays consistent in
app/[locale]/admin/sellers/create/page.tsx.
🧹 Nitpick comments (14)
app/[locale]/auth/signup/page.tsx (1)

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

Lógica de updateField duplicada respecto al nuevo hook useFormField.

Este bloque replica exactamente el updateField recién extraído en shared/presentation/use-form-field.ts (mismo setForm, misma limpieza de errors vía Object.hasOwn). Dado que esta cohorte introduce ese hook precisamente para eliminar esta duplicación (y app/[locale]/admin/sellers/create/page.tsx ya lo adopta), sería consistente que signup también migre a useFormField para form/loading/updateField, dejando solo updateAddressField como lógica local propia de esta página.

♻️ Fix propuesto: adoptar useFormField
+import { useFormField } from '`@/shared/presentation/use-form-field`';
 ...
-  const [form, setForm] = useState<FormState>({
-    firstName: '',
-    lastName: '',
-    email: '',
-    password: '',
-    confirmPassword: '',
-    address: { street: '', city: '', postalCode: '', country: '' },
-  });
   const [errors, setErrors] = useState<FormErrors>({});
   const [serverError, setServerError] = useState<string | null>(null);
-  const [loading, setLoading] = useState(false);
   const [showAddress, setShowAddress] = useState(false);
+  const { form, setForm, loading, setLoading, updateField } = useFormField(
+    {
+      firstName: '',
+      lastName: '',
+      email: '',
+      password: '',
+      confirmPassword: '',
+      address: { street: '', city: '', postalCode: '', country: '' },
+    },
+    errors,
+    setErrors,
+  );
-
-  const updateField = (field: keyof FormState, value: string) => {
-    setForm((prev) => ({ ...prev, [field]: value }));
-    if (Object.hasOwn(errors, field)) {
-      setErrors((prev) => {
-        const next = { ...prev };
-        delete next[field];
-        return next;
-      });
-    }
-  };
🤖 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]/auth/signup/page.tsx around lines 117 - 126, The signup page
still duplicates the same form state and field-update logic that was extracted
into useFormField. In app/[locale]/auth/signup/page.tsx, replace the local
form/updateField handling with the shared useFormField hook, following the
pattern already used in app/[locale]/admin/sellers/create/page.tsx; keep only
updateAddressField as page-specific logic and wire form/loading/updateField from
the hook.
shared/ui/input.tsx (1)

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

Considerar exportar InputClassNames.

InputClassNames no está exportada, lo que impide a consumidores externos tipar explícitamente sus propios objetos de override sin depender de la inferencia estructural desde un CSS module.

🤖 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/input.tsx` around lines 22 - 37, The InputProps API currently
references InputClassNames, but that type is not exported, preventing external
consumers from typing their own class override objects. Update the
shared/ui/input.tsx module so InputClassNames is exported alongside InputProps,
keeping the public typing surface explicit for consumers that need to reuse or
extend the input classNames shape.
shared/presentation/route-helpers.ts (2)

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

ZodSchema está deprecado en Zod v4.

Zod v4 marca ZodSchema como alias deprecado de ZodType. Sigue funcionando, pero conviene migrar para evitar warnings y quedar alineado con la API pública recomendada.

♻️ Sugerencia
-import type { ZodSchema } from 'zod';
+import type { ZodType } from 'zod';

 export async function parseBody<T>(
   request: NextRequest,
-  schema: ZodSchema<T>,
+  schema: ZodType<T>,
 ): Promise<T> {
🤖 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/route-helpers.ts` around lines 17 - 23, `parseBody` is
typed with the deprecated `ZodSchema` alias from Zod v4. Update the
`parseBody<T>` signature in `route-helpers` to use `ZodType<T>` instead, and
keep the existing `schema.parse(body)` behavior unchanged so the helper matches
the recommended public API and avoids deprecation warnings.

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

Los helpers unauthorized()/forbidden() no se usan en ninguna de las rutas refactorizadas.

Todas las rutas de este cohort (app/api/cart/checkout/confirm/route.ts, app/api/cart/checkout/route.ts, app/api/cart/route.ts, app/api/customizations/route.ts, app/api/customizations/[id]/route.ts) siguen construyendo manualmente NextResponse.json({ error: '...' }, { status: 401|403 }) en lugar de usar estos helpers recién creados. Dado que el objetivo declarado del PR es deduplicar código (jscpd), dejar estos helpers sin adoptar reintroduce la misma duplicación que se busca eliminar.

♻️ Ejemplo de adopción (aplicar en cada route)
-  if (!userId) {
-    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
-  }
+  if (!userId) {
+    return unauthorized();
+  }
🤖 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/route-helpers.ts` around lines 32 - 38, The new
unauthorized() and forbidden() helpers in route-helpers.ts are currently unused,
so the refactored routes still duplicate manual NextResponse.json error
responses. Update each affected route handler (checkout confirm, checkout, cart,
customizations, and customizations/[id]) to import and use unauthorized() and
forbidden() instead of constructing 401/403 responses inline, keeping the error
messages consistent while centralizing the response logic.
modules/customizations/presentation/to-customization-response.ts (1)

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

Tipo de designPosition duplicado inline.

Esta forma de designPosition replica una estructura que, según el stack del PR, se centraliza como value object/schema compartido (shared/kernel/domain/value-objects/design-position.ts / shared/validation/design-position-schema.ts). Mantener esta definición inline aquí puede divergir con el tiempo del tipo canónico. Si ese tipo/schema ya existe, considera reutilizarlo en lugar de redefinirlo.

🤖 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/to-customization-response.ts` around
lines 20 - 32, El tipo de designPosition está redefinido inline en
toCustomizationResponse y debería reutilizarse desde el value object/schema
compartido. Sustituye el casteo estructural actual por el tipo canónico de
design-position (por ejemplo, el export relacionado con design-position.ts o
design-position-schema.ts) para que toCustomizationResponse dependa de una única
fuente de verdad y no duplique la forma del objeto.
modules/cart/presentation/enrich-cart-item.ts (1)

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

Define un tipo explícito para la salida de enrichCartItem.
Record<string, unknown> oculta el contrato real de la respuesta. Extrae una interfaz concreta para este DTO enriquecido y reutilízala en las rutas que lo serializan; CartItemDTO actual no encaja con esta forma.

🤖 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/enrich-cart-item.ts` around lines 5 - 9, La salida
de enrichCartItem está tipada de forma demasiado genérica con Record<string,
unknown>, lo que oculta el contrato real del DTO enriquecido. Define una
interfaz explícita para este objeto de salida en enrichCartItem y úsala también
en las rutas que lo serializan; revisa el tipo CartItemDTO porque no corresponde
a esta forma y no debe reutilizarse aquí.
app/[locale]/admin/sellers/[sellerId]/products/page.tsx (1)

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

Lógica de paginación duplicada, no migrada al helper computePaginationState recién introducido.

Este bloque calcula manualmente page y hasProducts con la misma lógica que ahora vive en shared/presentation/pagination-utils.ts (computePaginationState), y que sí se usa en app/[locale]/seller/products/page.tsx (línea 77). Al no aplicarse aquí también, queda duplicación exactamente del tipo que este PR busca eliminar (jscpd).

♻️ Refactor propuesto
+import { computePaginationState } from '`@/shared/presentation/pagination-utils`';
...
   const { items: products, totalPages } = result;
-  let page = result.page;
-
-  if (totalPages > 0 && page > totalPages) {
-    page = totalPages;
-  }
-  const hasProducts = products.length > 0;
+  const { hasItems: hasProducts, currentPage: page } = computePaginationState(result);
🤖 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]/admin/sellers/[sellerId]/products/page.tsx around lines 51 -
57, The pagination logic in the products page is still being computed manually
instead of using computePaginationState, which leaves duplicated
page/hasProducts handling in this component. Update the page.tsx flow to call
the shared helper from shared/presentation/pagination-utils.ts, using the same
pattern already applied in app/[locale]/seller/products/page.tsx, and then
derive page and hasProducts from that returned pagination state instead of
reimplementing the checks locally.
modules/products/presentation/components/product-table-columns.tsx (1)

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

API redundante: labels y dict reciben el mismo objeto en todos los usos.

labels está tipado como un subconjunto de AdminDictionary y dict: AdminDictionary se usa solo para resolveStatusLabel. En ambos call sites (admin/sellers/[sellerId]/products/page.tsx y seller/products/page.tsx) se pasa dict.admin para ambos parámetros. Esto es redundante y abre la puerta a bugs futuros si algún consumidor pasa objetos distintos por error (p. ej. etiquetas de un locale y dict de otro).

Considera colapsar ambos parámetros en uno solo (dict: AdminDictionary) y derivar labels internamente desde dict.admin.

♻️ Refactor propuesto
 export function createProductTableColumns(
   locale: string,
-  labels: {
-    productName: string;
-    untranslatedProduct: string;
-    productStatus: string;
-    productPrice: string;
-    productUpdated: string;
-    actions: string;
-  },
   dict: AdminDictionary,
   styles: { nameCell?: string },
 ): DataTableColumn<ProductEntity>[] {
+  const labels = dict;
   return [
🤖 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-table-columns.tsx` around
lines 12 - 24, createProductTableColumns currently takes both labels and dict
even though both are always passed the same AdminDictionary-derived object;
simplify the API by removing the separate labels parameter and using dict:
AdminDictionary as the single source of truth. Update createProductTableColumns
to derive the table labels from dict.admin internally while keeping
resolveStatusLabel usage on the same dict, and then adjust the call sites in the
admin/sellers/[sellerId]/products/page.tsx and seller/products/page.tsx flows to
pass only dict.admin once.
modules/products/presentation/components/product-actions.tsx (1)

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

El parámetro status de renderStatusButton oculta el estado status del componente.

Ambos comparten nombre (const [status, setStatus] = useState(currentStatus); en línea 28 vs. el parámetro status en línea 100). Funcionalmente correcto ya que el helper usa consistentemente el parámetro, pero el shadowing dificulta la lectura y puede inducir errores en futuros refactors.

✏️ Sugerencia
   const renderStatusButton = (
     label: string,
     className: string,
-    status: string,
+    targetStatus: string,
   ) => (
     <button
       type="button"
       className={className}
       disabled={loading}
-      onClick={() => handleStatusChange(status)}
+      onClick={() => handleStatusChange(targetStatus)}
     >
       {loading ? dict.common.loading : label}
     </button>
   );
🤖 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-actions.tsx` around lines 97
- 110, The helper renderStatusButton shadows the component’s status state with
its own status parameter, which hurts readability and can cause confusion during
refactors. Rename the renderStatusButton parameter to something distinct like
nextStatus or targetStatus, and update the onClick handler to pass that renamed
value into handleStatusChange while keeping the existing status state untouched.
modules/presentation/components/design-preview.tsx (1)

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

Reutilizar DesignPositionData del kernel compartido.

Esta interfaz es idéntica a DesignPositionData en shared/kernel/domain/value-objects/design-position.ts. Dado que este PR busca deduplicar, conviene importarla en lugar de redefinirla localmente.

♻️ Refactor propuesto
+import type { DesignPositionData } from '`@/shared/kernel/domain/value-objects/design-position`';
-export interface DesignPositionData {
-  imageUrl: string;
-  x: number;
-  y: number;
-  scale: number;
-  rotation_deg: number;
-  opacity: number;
-  blend_mode: 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 `@modules/presentation/components/design-preview.tsx` around lines 10 - 18,
`DesignPositionData` is duplicated in `design-preview.tsx`; replace the local
interface with the shared type from
`shared/kernel/domain/value-objects/design-position` so the component reuses the
kernel definition instead of redefining it. Update the imports in
`DesignPreview` to reference the shared `DesignPositionData` symbol and remove
the local interface declaration.
modules/cart/presentation/cart-dto.ts (2)

44-45: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Cálculo de lineTotal repetido en múltiples archivos.

El patrón +(a * b).toFixed(2) se repite aquí y en cart-popup.tsx (líneas ~101-103, ~153) y cart-view.tsx (líneas ~157-158, ~184-185). Se podría extraer un helper computeLineTotal(unitPrice, quantity) en este mismo módulo para eliminar la duplicació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/cart-dto.ts` around lines 44 - 45, The lineTotal
calculation is duplicated across cart-dto.ts, cart-popup.tsx, and cart-view.tsx.
Extract a shared helper such as computeLineTotal(unitPrice, quantity) in
cart-dto.ts and reuse it everywhere those cart totals are built, replacing the
repeated +(a * b).toFixed(2) pattern in the relevant item-mapping logic.

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

Forma de designPosition duplicada respecto al value object compartido.

El shape inline de designPosition (7 campos) ya se repite en GuestCartItem.customizationDesignPosition (guest-cart-context.tsx) y en DesignPositionData (modules/presentation/components/design-preview.tsx). Según el stack de este PR, esta misma cohorte depende de un value object DesignPosition compartido (shared/kernel/domain/value-objects/design-position.ts) precisamente para evitar esta duplicación, pero cart-dto.ts no lo reutiliza. Dado que este PR introduce jscpd para detectar duplicación, este es un caso claro que debería consolidarse.

🤖 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/cart-dto.ts` around lines 20 - 28, The inline
designPosition shape in CartDto duplicates the shared DesignPosition value
object used elsewhere, so consolidate it by reusing the shared type instead of
repeating the 7-field object. Update the CartDto definition in cart-dto.ts to
reference DesignPosition from
shared/kernel/domain/value-objects/design-position.ts, and align any related
cart customization types such as GuestCartItem.customizationDesignPosition and
DesignPositionData to use the same shared symbol.
tests/unit/shared/presentation/components/file-upload-dropzone.test.tsx (1)

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

Ruta del archivo de test desactualizada respecto al componente movido.

El import ahora apunta a @/shared/ui/file-upload-dropzone, pero el archivo de test sigue ubicado en tests/unit/shared/presentation/components/. Considera moverlo a tests/unit/shared/ui/file-upload-dropzone.test.tsx para mantener la estructura de tests alineada con la nueva ubicación del código fuente.

🤖 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/shared/presentation/components/file-upload-dropzone.test.tsx` at
line 3, The test file location is still aligned with the old component
structure, so move the FileUploadDropzone spec to match the new shared/ui
placement. Update the test path from the old presentation/components area to the
corresponding shared/ui/file-upload-dropzone.test.tsx location, keeping the
import of FileUploadDropzone from '`@/shared/ui/file-upload-dropzone`' unchanged
and ensuring the test folder mirrors the component’s current source location.
modules/cart/presentation/guest-cart-context.tsx (1)

168-172: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

customizationDesignPosition se castea sin validación de forma.

applyCustomizationFields acepta customization.designPosition como Record<string, unknown> y lo castea directamente a GuestCartItem['customizationDesignPosition'] sin validar su forma. Dado que este dato viene de localStorage (puede haber sido manipulado o quedar corrupto entre versiones) y que este mismo PR introduce un designPositionSchema compartido (Zod) en una capa dependiente de esta cohorte, sería más robusto parsear/validar con ese esquema en lugar de un cast ciego.

🤖 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 168 - 172,
`applyCustomizationFields` is casting `customization.designPosition` directly to
`GuestCartItem['customizationDesignPosition']` without validating its shape.
Update this logic to parse/validate the value with the shared
`designPositionSchema` instead of using a blind cast, and fall back to
`item.customizationDesignPosition` when the schema check fails or the field is
absent. Use the existing `applyCustomizationFields` and `designPositionSchema`
symbols to locate and wire the fix.
🤖 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 @.gitignore:
- Around line 43-44: The .gitignore rule for the jscpd report is pointing to the
wrong output path, so the generated artifact still appears in diffs. Update the
ignore entry from the generic /reports path to the actual directory used for the
jscpd output, and keep the change scoped near the existing jscpd ignore block so
it matches the generated report location.

In `@modules/cart/presentation/cart-dto.ts`:
- Around line 13-29: El tipo CartItemDTO.customization no contempla que en la
práctica puede ser null, lo que deja una brecha entre el DTO y los consumidores.
Actualiza la definición en cart-dto.ts para que customization sea nullable, y
revisa los usos en cart-popup.tsx y cart-view.tsx para manejar explícitamente el
caso null en lugar de depender de as CartItemDTO o de accesos directos a
propiedades.

In `@modules/cart/presentation/components/cart-view.tsx`:
- Around line 126-134: The guest item mapping in cart-view.tsx is overriding the
DTO id with productId, which breaks per-line identity for personalized guest
cart items. Update the CartItemDTO construction in the guest branch so it keeps
the id coming from guestItemToDTO, and then make sure the cart actions in
CartView align with that stable line-item id instead of productId. Use the
existing symbols guestItemToDTO, CartItemDTO, updateQuantity, removeItem, and
the item.id key usage to locate and keep the identity consistent.

In `@modules/orders/presentation/order-page-url.ts`:
- Around line 14-18: The fallback page size is inconsistent with
orderListQuerySchema, causing generated URLs and pagination defaults to drift.
Update the order page URL flow in order-page-url and the two order page entry
points to use the same shared default pageSize value instead of hardcoding 10 or
20. Extract the defaults into a shared constant and have orderListQuerySchema,
the page.tsx fallback handling, and the URL builder all reference it so pageSize
stays aligned everywhere.

In `@report/jscpd-report.html`:
- Around line 1-691: The generated jscpd report HTML needs to be reformatted to
satisfy Prettier before merge. Re-run the report generator or apply the repo’s
expected formatting to the full document, keeping the structure and content in
report/jscpd-report.html intact while making it Prettier-compliant. Focus on the
overall HTML/CSS/JS in the report output rather than changing the report data.
- Around line 95-103: The javascript row in the jscpd report has a broken anchor
link to javascript-clones, so update the table entry to match the actual
document structure. If there is no javascript-clones section in the report,
remove the hyperlink from that row in the report HTML; if the section should
exist, add the matching anchor/section so the href target resolves correctly.

In `@shared/authorization/session-user-context.ts`:
- Around line 10-18: getSessionUserContext() currently returns a context even
when session.user has no resolvable userId, which can let downstream code
proceed with an empty identifier. Update getSessionUserContext in
session-user-context.ts to fail closed by checking the resolved userId after
reading session.user and returning null unless it is present and non-empty; keep
the existing SessionUserContext shape otherwise so callers like the uploads
route never reach deleteUpload.execute with an invalid userId.

In `@shared/presentation/canvas-utils.ts`:
- Around line 35-54: drawDesign is mutating the canvas context without restoring
it, so subsequent renders can inherit the translate/rotate state. Update
drawDesign in canvas-utils.ts to wrap its transform and drawImage calls with
ctx.save() and ctx.restore() inside the function, using the drawDesign symbol to
isolate the context state even when it is called directly from
design-preview.tsx without external save/restore.

In `@shared/presentation/route-helpers.ts`:
- Around line 25-30: `getCurrentSellerId` is conflating anonymous access with an
authenticated user who has no seller record. Update `getCurrentSellerId` in
`route-helpers.ts` to return a distinguishable result for “not authenticated”
versus “authenticated but no seller,” rather than always `null`. Then adjust the
callers in the customization route handlers to map anonymous requests to 401
Unauthorized and authenticated non-seller requests to 403 Forbidden, using the
new return shape/sentinel consistently.

In `@shared/ui/input.tsx`:
- Around line 11-20: The Input component currently replaces baseStyles entirely
when classNames is provided, which breaks partial overrides and can leave
template interpolations like s.input, s.inputError, and s.hasSuffix as
undefined. Update the InputClassNames handling in Input so it merges classNames
over baseStyles by key instead of using a wholesale fallback, and ensure the
className construction in the Input render path only uses defined strings. Keep
the fix scoped around the Input component and its classNames/baseStyles
resolution.

In `@shared/ui/text-field.tsx`:
- Around line 3-8: `TextField` is overwriting any incoming `classNames` by
always passing `styles` to `Input`, so update the `TextField` component to
preserve caller-supplied classes instead of replacing them. Either remove
`classNames` from the `InputProps` accepted by `TextField` if customization
should not be allowed, or merge the incoming `props.classNames` with `styles`
before forwarding to `Input`; use `TextField` and `Input` as the key symbols to
locate the fix.

---

Outside diff comments:
In `@app/`[locale]/admin/sellers/create/page.tsx:
- Around line 102-130: The submit handler in handleSubmit recalculates
normalizePayload(form) twice, once before validateFormGeneric and again inside
the try block, which is redundant and shadows the existing payload variable.
Reuse the already computed payload for both validation and the subsequent submit
logic, and remove the second normalizePayload(form) call so the data path stays
consistent in app/[locale]/admin/sellers/create/page.tsx.

---

Nitpick comments:
In `@app/`[locale]/admin/sellers/[sellerId]/products/page.tsx:
- Around line 51-57: The pagination logic in the products page is still being
computed manually instead of using computePaginationState, which leaves
duplicated page/hasProducts handling in this component. Update the page.tsx flow
to call the shared helper from shared/presentation/pagination-utils.ts, using
the same pattern already applied in app/[locale]/seller/products/page.tsx, and
then derive page and hasProducts from that returned pagination state instead of
reimplementing the checks locally.

In `@app/`[locale]/auth/signup/page.tsx:
- Around line 117-126: The signup page still duplicates the same form state and
field-update logic that was extracted into useFormField. In
app/[locale]/auth/signup/page.tsx, replace the local form/updateField handling
with the shared useFormField hook, following the pattern already used in
app/[locale]/admin/sellers/create/page.tsx; keep only updateAddressField as
page-specific logic and wire form/loading/updateField from the hook.

In `@modules/cart/presentation/cart-dto.ts`:
- Around line 44-45: The lineTotal calculation is duplicated across cart-dto.ts,
cart-popup.tsx, and cart-view.tsx. Extract a shared helper such as
computeLineTotal(unitPrice, quantity) in cart-dto.ts and reuse it everywhere
those cart totals are built, replacing the repeated +(a * b).toFixed(2) pattern
in the relevant item-mapping logic.
- Around line 20-28: The inline designPosition shape in CartDto duplicates the
shared DesignPosition value object used elsewhere, so consolidate it by reusing
the shared type instead of repeating the 7-field object. Update the CartDto
definition in cart-dto.ts to reference DesignPosition from
shared/kernel/domain/value-objects/design-position.ts, and align any related
cart customization types such as GuestCartItem.customizationDesignPosition and
DesignPositionData to use the same shared symbol.

In `@modules/cart/presentation/enrich-cart-item.ts`:
- Around line 5-9: La salida de enrichCartItem está tipada de forma demasiado
genérica con Record<string, unknown>, lo que oculta el contrato real del DTO
enriquecido. Define una interfaz explícita para este objeto de salida en
enrichCartItem y úsala también en las rutas que lo serializan; revisa el tipo
CartItemDTO porque no corresponde a esta forma y no debe reutilizarse aquí.

In `@modules/cart/presentation/guest-cart-context.tsx`:
- Around line 168-172: `applyCustomizationFields` is casting
`customization.designPosition` directly to
`GuestCartItem['customizationDesignPosition']` without validating its shape.
Update this logic to parse/validate the value with the shared
`designPositionSchema` instead of using a blind cast, and fall back to
`item.customizationDesignPosition` when the schema check fails or the field is
absent. Use the existing `applyCustomizationFields` and `designPositionSchema`
symbols to locate and wire the fix.

In `@modules/customizations/presentation/to-customization-response.ts`:
- Around line 20-32: El tipo de designPosition está redefinido inline en
toCustomizationResponse y debería reutilizarse desde el value object/schema
compartido. Sustituye el casteo estructural actual por el tipo canónico de
design-position (por ejemplo, el export relacionado con design-position.ts o
design-position-schema.ts) para que toCustomizationResponse dependa de una única
fuente de verdad y no duplique la forma del objeto.

In `@modules/presentation/components/design-preview.tsx`:
- Around line 10-18: `DesignPositionData` is duplicated in `design-preview.tsx`;
replace the local interface with the shared type from
`shared/kernel/domain/value-objects/design-position` so the component reuses the
kernel definition instead of redefining it. Update the imports in
`DesignPreview` to reference the shared `DesignPositionData` symbol and remove
the local interface declaration.

In `@modules/products/presentation/components/product-actions.tsx`:
- Around line 97-110: The helper renderStatusButton shadows the component’s
status state with its own status parameter, which hurts readability and can
cause confusion during refactors. Rename the renderStatusButton parameter to
something distinct like nextStatus or targetStatus, and update the onClick
handler to pass that renamed value into handleStatusChange while keeping the
existing status state untouched.

In `@modules/products/presentation/components/product-table-columns.tsx`:
- Around line 12-24: createProductTableColumns currently takes both labels and
dict even though both are always passed the same AdminDictionary-derived object;
simplify the API by removing the separate labels parameter and using dict:
AdminDictionary as the single source of truth. Update createProductTableColumns
to derive the table labels from dict.admin internally while keeping
resolveStatusLabel usage on the same dict, and then adjust the call sites in the
admin/sellers/[sellerId]/products/page.tsx and seller/products/page.tsx flows to
pass only dict.admin once.

In `@shared/presentation/route-helpers.ts`:
- Around line 17-23: `parseBody` is typed with the deprecated `ZodSchema` alias
from Zod v4. Update the `parseBody<T>` signature in `route-helpers` to use
`ZodType<T>` instead, and keep the existing `schema.parse(body)` behavior
unchanged so the helper matches the recommended public API and avoids
deprecation warnings.
- Around line 32-38: The new unauthorized() and forbidden() helpers in
route-helpers.ts are currently unused, so the refactored routes still duplicate
manual NextResponse.json error responses. Update each affected route handler
(checkout confirm, checkout, cart, customizations, and customizations/[id]) to
import and use unauthorized() and forbidden() instead of constructing 401/403
responses inline, keeping the error messages consistent while centralizing the
response logic.

In `@shared/ui/input.tsx`:
- Around line 22-37: The InputProps API currently references InputClassNames,
but that type is not exported, preventing external consumers from typing their
own class override objects. Update the shared/ui/input.tsx module so
InputClassNames is exported alongside InputProps, keeping the public typing
surface explicit for consumers that need to reuse or extend the input classNames
shape.

In `@tests/unit/shared/presentation/components/file-upload-dropzone.test.tsx`:
- Line 3: The test file location is still aligned with the old component
structure, so move the FileUploadDropzone spec to match the new shared/ui
placement. Update the test path from the old presentation/components area to the
corresponding shared/ui/file-upload-dropzone.test.tsx location, keeping the
import of FileUploadDropzone from '`@/shared/ui/file-upload-dropzone`' unchanged
and ensuring the test folder mirrors the component’s current source location.
🪄 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: 8460d51d-ee6b-44d9-be37-25743142b646

📥 Commits

Reviewing files that changed from the base of the PR and between cf7d6c9 and 9e868ca.

⛔ Files ignored due to path filters (35)
  • tests/unit/app/[locale]/seller/products/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json is excluded by !**/node_modules/**
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0424fd7c-41cf-48ee-8215-2a1f7636c1f9.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/0ba87ba4-8a04-478b-88a2-1ecc490dabbc.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/43150e8a-f92c-4d99-b36f-9ad8b12c92fd.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/54b3f06b-2044-456a-9757-75505771f677.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/7072116f-5974-42f6-8344-23861596b58e.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/8299071a-2631-4a3e-bc8f-cdba90a76c3d.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/86912d37-6fc1-4f24-8ff5-fae00ff1eb12.png is excluded by !**/*.png
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ade99071-3c02-4ed4-b640-f96aa3dd2db4.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/c2421aa8-a506-4e1e-b738-e47e64d8956c.jpg is excluded by !**/*.jpg
  • tmp/uploads/customization/guest_7453429277c9a47570419b48e4edb8f8/ddac64c9-c822-4631-93f4-e1f1133caa04.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/0770179d-365d-4b8a-b278-453d1e6b717e.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/51d82e86-9260-4eab-9fac-77513ad454cd.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/72e1a4df-88ce-4d82-ae8e-300a65f8c76d.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/7748b1c2-6b6d-4dc2-b3d5-f8717a581d6c.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/859fae6e-2c78-46ee-b025-1fd8048e87b5.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ed250a15-8870-40f6-a1ec-8a8845ada3ed.png is excluded by !**/*.png
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/ef719e39-2897-4106-ad16-a2339739f06a.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/34162f3a-0a1e-4c92-8088-99db5e0cd3e9.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/c9abdded-5e1b-4697-b236-d3380155bb5c.png is excluded by !**/*.png
  • tmp/uploads/product/cmr23m83f0001iotkjny1yugb/cfe23339-3140-4855-99d0-cbf16d3b8412.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/530f7dac-9b5f-4a14-adc3-717a7f603532.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/9026017f-9c52-4e5b-b6be-2c8e8649caf6.jpg is excluded by !**/*.jpg
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/94a8b1bb-3778-4cfa-80f2-38e2778cf1c2.png is excluded by !**/*.png
  • tmp/uploads/product/cmr3knpdw0001k4tk7z1wpewq/a8dfbda4-a431-406d-835e-9bd18e9f7eb2.png is excluded by !**/*.png
  • tmp/uploads/product/cmr4su2n40001hstky222wdt1/0b154df8-be9c-4f79-a72c-08c31981c3d1.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/14e368b7-4346-4b2e-baab-e110164b1351.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/21483e63-efe8-4b4b-8e4a-c0df35590712.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/38335571-204b-4d41-afbf-57f184795e17.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/3b5ecc3a-5682-4139-a58c-16f93e125dc6.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/422c64a2-09ab-4a68-9cec-343d29d8db8a.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/8582fb4e-11e8-4c36-91ee-fcfc31865de4.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/bb3042a4-5dbb-462e-9f44-a0f6b0316de8.png is excluded by !**/*.png
  • tmp/uploads/product/cmr57cv99000150tkl5tiw1zp/bc80da5f-cab1-4c54-b31c-b7db80ff1b5c.png is excluded by !**/*.png
  • tmp/uploads/product/cmr5b53wa0001sctk5nxspoc6/98354535-e062-4ad3-b5b2-6f7ee6d590c4.png is excluded by !**/*.png
📒 Files selected for processing (88)
  • .gitignore
  • .jscpd.json
  • app/[locale]/admin/sellers/[sellerId]/products/page.tsx
  • app/[locale]/admin/sellers/create/page.tsx
  • app/[locale]/auth/signup/page.tsx
  • app/[locale]/cart/design-preview.tsx
  • app/[locale]/orders/page.tsx
  • app/[locale]/products/[id]/mockup-canvas-control.tsx
  • app/[locale]/seller/orders/page.tsx
  • app/[locale]/seller/products/[id]/edit/page.tsx
  • app/[locale]/seller/products/new/page.tsx
  • app/[locale]/seller/products/page.tsx
  • app/[locale]/seller/products/product-photo-gallery.tsx
  • app/api/cart/checkout/confirm/route.ts
  • app/api/cart/checkout/route.ts
  • app/api/cart/items/[itemId]/route.ts
  • app/api/cart/items/route.ts
  • app/api/cart/migrate/route.ts
  • app/api/cart/route.ts
  • app/api/customizations/[id]/route.ts
  • app/api/customizations/customer/route.ts
  • app/api/customizations/route.ts
  • app/api/sellers/[id]/route.ts
  • app/api/sellers/route.ts
  • app/api/uploads/[id]/route.ts
  • components/cart/add-to-cart-button.tsx
  • components/cart/add-to-cart-choice-modal.module.css
  • components/cart/add-to-cart-choice-modal.tsx
  • components/cart/customization-draft-schema.ts
  • composition-root/container.ts
  • modules/cart/application/cart-access.ts
  • modules/cart/application/remove-cart-item.ts
  • modules/cart/application/update-cart-item.ts
  • modules/cart/infrastructure/customization-lookup-adapter.ts
  • modules/cart/presentation/cart-dto.ts
  • modules/cart/presentation/cart-events.ts
  • modules/cart/presentation/components/add-to-cart-button.tsx
  • modules/cart/presentation/components/cart-icon.tsx
  • modules/cart/presentation/components/cart-merge-detector.tsx
  • modules/cart/presentation/components/cart-popup.tsx
  • modules/cart/presentation/components/cart-view.tsx
  • modules/cart/presentation/components/customization-draft-schema.ts
  • modules/cart/presentation/components/merge-dialog.tsx
  • modules/cart/presentation/enrich-cart-item.ts
  • modules/cart/presentation/guest-cart-context.tsx
  • modules/customizations/infrastructure/prisma-customization-repository.ts
  • modules/customizations/presentation/schemas/customization-schemas.ts
  • modules/customizations/presentation/to-customization-response.ts
  • modules/email/infrastructure/brevo-email-sender.ts
  • modules/orders/infrastructure/prisma-order-repository.ts
  • modules/orders/presentation/components/order-table-columns.tsx
  • modules/orders/presentation/order-page-url.ts
  • modules/orders/presentation/order-status-labels.ts
  • modules/presentation/components/design-preview.tsx
  • modules/products/presentation/components/product-actions.tsx
  • modules/products/presentation/components/product-table-columns.tsx
  • modules/products/presentation/product-form-labels.ts
  • modules/sellers/presentation/seller-response.ts
  • modules/users/application/use-cases/register-user-use-case.ts
  • modules/users/application/use-cases/update-user-use-case.ts
  • report/jscpd-report.html
  • shared/authorization/session-user-context.ts
  • shared/infrastructure/outbox-service.ts
  • shared/kernel/domain/identifiers/entity-id.ts
  • shared/kernel/domain/identifiers/index.ts
  • shared/kernel/domain/value-objects/design-position.ts
  • shared/presentation/canvas-utils.ts
  • shared/presentation/components/file-upload-dropzone.module.css
  • shared/presentation/components/file-upload-dropzone.tsx
  • shared/presentation/pagination-utils.ts
  • shared/presentation/route-helpers.ts
  • shared/presentation/use-form-field.ts
  • shared/ui/input.tsx
  • shared/ui/text-field.tsx
  • shared/validation/design-position-schema.ts
  • shared/validation/name-validator.ts
  • shared/validation/validate-form.ts
  • tests/unit/app/[locale]/page.test.tsx
  • tests/unit/composition-root/container-seller-binding.test.ts
  • tests/unit/shared/infrastructure/outbox-service.test.ts
  • tests/unit/shared/presentation/components/file-upload-dropzone.test.tsx
  • tmp/check-db.ts
  • tmp/eslint-out.json
  • tmp/eslint-out2.json
  • tmp/eslint-out3.json
  • tmp/eslint-out4.json
  • tmp/uploads/product/cmr1z8b430001jctkkw7afqoq/41d48cda-1463-4903-a890-a75dc70860d2.webp
  • workers/outbox-worker.ts
💤 Files with no reviewable changes (16)
  • components/cart/add-to-cart-button.tsx
  • modules/email/infrastructure/brevo-email-sender.ts
  • shared/kernel/domain/identifiers/index.ts
  • components/cart/customization-draft-schema.ts
  • shared/presentation/components/file-upload-dropzone.module.css
  • tmp/eslint-out2.json
  • shared/kernel/domain/identifiers/entity-id.ts
  • components/cart/add-to-cart-choice-modal.module.css
  • tmp/eslint-out4.json
  • shared/infrastructure/outbox-service.ts
  • tmp/eslint-out3.json
  • app/[locale]/cart/design-preview.tsx
  • tmp/eslint-out.json
  • tmp/check-db.ts
  • shared/presentation/components/file-upload-dropzone.tsx
  • components/cart/add-to-cart-choice-modal.tsx

Comment thread .gitignore Outdated
Comment on lines +13 to +29
customization: {
text: string | null;
color: string | null;
size: string | null;
imageUrl: string | null;
imageUploadId?: string | null;
colorImageUrl?: string | null;
designPosition?: {
imageUrl: string;
x: number;
y: number;
scale: number;
rotation_deg: number;
opacity: number;
blend_mode: string;
} | null;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El tipo CartItemDTO.customization no refleja que puede ser null en la práctica.

customization está tipado como un objeto obligatorio, pero los consumidores del mismo DTO (por ejemplo modules/cart/presentation/components/cart-popup.tsx, que hace fetch('/api/cart') y mapea la respuesta) asignan customization: null cuando el ítem no tiene personalización, forzando el tipo con as CartItemDTO. Esto rompe la garantía de tipos: cualquier consumidor que no use encadenamiento opcional (como buildCustomizationHref en cart-view.tsx, que accede a customization.text, customization.color, etc. sin ?.) podría lanzar un TypeError en tiempo de ejecución si recibe customization: null desde el servidor.

Sugerencia: tipar customization como nullable (customization: {...} | null) para que el compilador obligue a los consumidores a manejar el caso null explícitamente, eliminando la necesidad del cast as CartItemDTO.

🛠️ Fix propuesto
 export interface CartItemDTO {
   id: string;
   productId: string;
   productName: string;
   productImageUrl: string | null;
   sellerId: string;
   sellerName: string;
   quantity: number;
   unitPrice: number;
   lineTotal: number;
-  customization: {
+  customization: {
     text: string | null;
     color: string | null;
     size: string | null;
     imageUrl: string | null;
     imageUploadId?: string | null;
     colorImageUrl?: string | null;
     designPosition?: {
       imageUrl: string;
       x: number;
       y: number;
       scale: number;
       rotation_deg: number;
       opacity: number;
       blend_mode: string;
     } | null;
-  };
+  } | null;
 }
📝 Committable suggestion

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

Suggested change
customization: {
text: string | null;
color: string | null;
size: string | null;
imageUrl: string | null;
imageUploadId?: string | null;
colorImageUrl?: string | null;
designPosition?: {
imageUrl: string;
x: number;
y: number;
scale: number;
rotation_deg: number;
opacity: number;
blend_mode: string;
} | null;
};
customization: {
text: string | null;
color: string | null;
size: string | null;
imageUrl: string | null;
imageUploadId?: string | null;
colorImageUrl?: string | null;
designPosition?: {
imageUrl: string;
x: number;
y: number;
scale: number;
rotation_deg: number;
opacity: number;
blend_mode: string;
} | null;
} | 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/cart/presentation/cart-dto.ts` around lines 13 - 29, El tipo
CartItemDTO.customization no contempla que en la práctica puede ser null, lo que
deja una brecha entre el DTO y los consumidores. Actualiza la definición en
cart-dto.ts para que customization sea nullable, y revisa los usos en
cart-popup.tsx y cart-view.tsx para manejar explícitamente el caso null en lugar
de depender de as CartItemDTO o de accesos directos a propiedades.

Comment thread modules/cart/presentation/components/cart-view.tsx
Comment thread modules/orders/presentation/order-page-url.ts Outdated
Comment thread report/jscpd-report.html Outdated
Comment on lines +1 to +691
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Copy/Paste Detector Report</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f7fafc; color: #2d3748; }
a { color: #3182ce; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { background: #fff; box-shadow: 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06); padding: 1rem 0; }
.header-inner { max-width: 80rem; margin: 0 auto; padding: 0 1rem; }
.header h1 { font-size: 1.875rem; font-weight: 600; color: #2d3748; margin: 0; }
.main { max-width: 80rem; margin: 2rem auto; padding: 0 1rem; }
.card { background: #fff; box-shadow: 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06); border-radius: 0.25rem; padding: 1.5rem; margin-bottom: 2rem; }
h2 { font-size: 1.5rem; font-weight: 600; color: #4a5568; margin: 0 0 1rem 0; }
.dashboard { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
.stat-card { padding: 1rem; border-radius: 0.25rem; text-align: center; }
.stat-card h3 { font-size: 1.125rem; font-weight: 600; margin: 0 0 0.5rem 0; }
.stat-card .val { font-size: 2.25rem; font-weight: 700; }
.stat-blue { background: #bee3f8; color: #2a4365; }
.stat-green { background: #c6f6d5; color: #276749; }
.stat-yellow { background: #fefcbf; color: #975a16; }
.stat-red { background: #fed7d7; color: #9b2c2c; }
table { width: 100%; border-collapse: collapse; }
thead th { background: #edf2f7; color: #718096; text-transform: uppercase; font-size: 0.75rem; font-weight: 600; padding: 0.75rem 1.5rem; text-align: left; }
tbody td { padding: 0.75rem 1.5rem; border-bottom: 1px solid #edf2f7; font-size: 0.875rem; }
tbody tr:hover { background: #f7fafc; }
.clone-section { margin-bottom: 2rem; }
.clone-section h2 { border-bottom: 2px solid #edf2f7; padding-bottom: 0.5rem; }
.clone-item { padding: 1rem 0; border-bottom: 1px solid #edf2f7; }
.clone-item:last-child { border-bottom: none; }
.clone-info { color: #718096; font-size: 0.875rem; margin-bottom: 0.25rem; }
.toggle-btn { background: #a0aec0; color: #fff; border: none; padding: 0.125rem 0.5rem; font-size: 0.75rem; border-radius: 0.25rem; cursor: pointer; margin-left: 0.5rem; }
.toggle-btn:focus { outline: none; }
.code-block { background: #f7fafc; border: 1px solid #edf2f7; border-radius: 0.25rem; padding: 1rem; margin-top: 0.5rem; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.875rem; color: #2d3748; white-space: pre-wrap; overflow-x: auto; display: none; }
.code-block.visible { display: block; }
.no-dupes { color: #27ae60; font-weight: 700; font-size: 1.125rem; }
.footer { margin-top: 3rem; padding: 2rem 0; border-top: 1px solid #e0e0e0; text-align: center; font-family: -apple-system, sans-serif; color: #666; }
.footer p { margin: 0.5rem 0; font-size: 0.875rem; }
.footer a { color: #0066cc; }
.footer .small { font-size: 0.75rem; color: #999; }
@media (max-width: 768px) { .dashboard { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>
<body>
<header class="header">
<div class="header-inner">
<h1>jscpd - copy/paste report</h1>
</div>
</header>

<div class="main">
<div class="card">
<section>
<h2>Dashboard</h2>
<div class="dashboard">
<div class="stat-card stat-blue">
<h3>Total Files</h3>
<div class="val">317</div>
</div>
<div class="stat-card stat-green">
<h3>Total Lines of Code</h3>
<div class="val">28062</div>
</div>
<div class="stat-card stat-yellow">
<h3>Number of Clones</h3>
<div class="val">18</div>
</div>
<div class="stat-card stat-red">
<h3>Duplicated Lines</h3>
<div class="val">322 (1.15%)</div>
</div>
</div>
</section>
</div>

<div class="card">
<section>
<h2>Formats with Duplications</h2>
<table>
<thead>
<tr>
<th>Format</th>
<th>Files</th>
<th>Lines</th>
<th>Clones</th>
<th>Duplicated Lines</th>
<th>Duplicated Tokens</th>
</tr>
</thead>
<tbody>

<tr>
<td><a href="#javascript-clones">javascript</a></td>
<td>2</td>
<td>412</td>
<td>0</td>
<td>0 (0.00%)</td>
<td>0 (0.00%)</td>
</tr>

<tr>
<td><a href="#tsx-clones">tsx</a></td>
<td>83</td>
<td>10625</td>
<td>6</td>
<td>105 (0.99%)</td>
<td>522 (0.92%)</td>
</tr>

<tr>
<td><a href="#typescript-clones">typescript</a></td>
<td>232</td>
<td>17025</td>
<td>12</td>
<td>217 (1.27%)</td>
<td>1122 (1.42%)</td>
</tr>

<tr style="font-weight: 700;">
<td>Total</td>
<td>317</td>
<td>28062</td>
<td>18</td>
<td>322 (1.15%)</td>
<td>1644 (1.20%)</td>
</tr>
</tbody>
</table>
</section>
</div>



<div class="card">
<section class="clone-section">
<a name="tsx-clones"></a>
<h2>tsx</h2>
<div>

<div class="clone-item">
<div class="clone-info">
app\[locale]\orders\page.tsx (Line 93:15 - Line 107:27),
app\[locale]\seller\orders\page.tsx (Line 122:13 - Line 136:27)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-1')">Show code</button>
</div>
<pre id="code-tsx-1" class="code-block"> &lt;/form&gt;
)}
&lt;/div&gt;
),
},
];

const { hasItems: hasOrders, currentPage } = computePaginationState(result);

return (
&lt;div className={styles.container}&gt;
&lt;div className={styles.header}&gt;
&lt;div className={styles.headerLead}&gt;
&lt;h1 className={styles.title}&gt;
{dict.orders?.myOrders ?? &#x27;My orders&#x27;}</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\[locale]\orders\page.tsx (Line 121:14 - Line 136:62),
app\[locale]\seller\orders\page.tsx (Line 183:15 - Line 198:62)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-2')">Show code</button>
</div>
<pre id="code-tsx-2" class="code-block"> &lt;/div&gt;
&lt;/div&gt;

{hasOrders ? (
&lt;&gt;
&lt;div className={styles.tableWrap}&gt;
&lt;DataTable
columns={columns}
rows={result.items}
rowKey={(o) =&gt; o.id}
/&gt;
&lt;/div&gt;
&lt;Pagination
currentPage={currentPage}
totalPages={result.totalPages}
buildPageUrl={(page) =&gt; buildOrderPageUrl(locale, &#x27;/orders&#x27;, filter, page)}</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\[locale]\orders\page.tsx (Line 136:72 - Line 148:2),
app\[locale]\seller\orders\page.tsx (Line 198:79 - Line 210:2)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-3')">Show code</button>
</div>
<pre id="code-tsx-3" class="code-block"> buildPageUrl={(page) =&gt; buildOrderPageUrl(locale, &#x27;/orders&#x27;, filter, page)}
prevLabel={dict.orders?.previous ?? &#x27;← Previous&#x27;}
nextLabel={dict.orders?.next ?? &#x27;Next →&#x27;}
/&gt;
&lt;/&gt;
) : (
&lt;Card className={styles.emptyState}&gt;
{dict.orders?.noOrders ?? &#x27;No orders yet&#x27;}
&lt;/Card&gt;
)}
&lt;/div&gt;
);
}</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\[locale]\products\[id]\customization-experience.tsx (Line 45:31 - Line 62:36),
app\[locale]\products\[id]\customization-form.tsx (Line 24:33 - Line 41:36)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-4')">Show code</button>
</div>
<pre id="code-tsx-4" class="code-block"> customizationInvalidImageUrl: string;
customizationCanvasLabel: string;
customizationCanvasHelp: string;
customizationProductImageAlt: string;
customizationDesignImageAlt: string;
customizationUploadDesign: string;
customizationReplaceDesign: string;
customizationRemoveDesign: string;
customizationDesignUploading: string;
customizationDesignInvalid: string;
customizationDesignTooLarge: string;
customizationScaleLabel: string;
customizationRotationLabel: string;
customizationOpacityLabel: string;
customizationPositionReadoutLabel: string;
customizationPositionXLabel: string;
customizationPositionYLabel: string;
customizationCanvasReset: string;</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\cart\presentation\components\add-to-cart-button.tsx (Line 283:26 - Line 300:39),
modules\cart\presentation\components\add-to-cart-button.tsx (Line 361:34 - Line 377:39)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-5')">Show code</button>
</div>
<pre id="code-tsx-5" class="code-block"> if (isAuthenticated) {
const customizationResponse = await fetch(
&#x27;/api/customizations/customer&#x27;,
{
method: &#x27;POST&#x27;,
headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; },
body: JSON.stringify({
productId,
text: draft.text,
color: draft.color,
size: draft.size,
imageUrl: draft.imageUrl,
designPosition: draft.designPosition ?? null,
}),
},
);

if (!customizationResponse.ok) {</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\cart\presentation\components\add-to-cart-button.tsx (Line 487:38 - Line 517:8),
modules\cart\presentation\components\add-to-cart-button.tsx (Line 521:38 - Line 551:8)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-tsx-6')">Show code</button>
</div>
<pre id="code-tsx-6" class="code-block"> const newQty = currentQuantity + 1;
if (isAuthenticated &amp;&amp; cartItemInfo) {
const prevCartItemInfo = cartItemInfo;
setCartItemInfo({ ...cartItemInfo, quantity: newQty });
try {
const res = await fetch(`/api/cart/items/${cartItemInfo.cartItemId}`, {
method: &#x27;PATCH&#x27;,
headers: { &#x27;Content-Type&#x27;: &#x27;application/json&#x27; },
body: JSON.stringify({ quantity: newQty }),
});
if (!res.ok) {
setCartItemInfo(prevCartItemInfo);
return;
}
dispatchCartUpdated();
} catch {
setCartItemInfo(prevCartItemInfo);
}
} else if (guestMatch?.id) {
updateItemQuantity(guestMatch.id, newQty);
}
}, [
isInCart,
currentQuantity,
isAuthenticated,
cartItemInfo,
guestMatch,
updateItemQuantity,
]);

const handleDecrement = useCallback(async () =&gt; {</pre>
</div>

</div>
</section>
</div>

<div class="card">
<section class="clone-section">
<a name="typescript-clones"></a>
<h2>typescript</h2>
<div>

<div class="clone-item">
<div class="clone-info">
app\api\auth\forgot-password\route.ts (Line 45:47 - Line 55:59),
app\api\auth\reset-password\route.ts (Line 50:44 - Line 60:59)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-1')">Show code</button>
</div>
<pre id="code-typescript-1" class="code-block"> await rateLimiter.recordLoginAttempt(email, ip, true);

return NextResponse.json(result);
} catch (error: unknown) {
// Record rate-limiting on errors too — every request should count
try {
const ip =
req.headers.get(&#x27;x-forwarded-for&#x27;)?.split(&#x27;,&#x27;, 1)[0]?.trim() ??
req.headers.get(&#x27;x-real-ip&#x27;) ??
&#x27;unknown&#x27;;
await container.getRateLimiter().recordLoginAttempt(&#x27;unknown&#x27;, ip, false);</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\cart\checkout\confirm\route.ts (Line 7:65 - Line 36:47),
app\api\cart\items\route.ts (Line 7:78 - Line 31:47)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-2')">Show code</button>
</div>
<pre id="code-typescript-2" class="code-block">import { PriceChangedError } from &#x27;@/modules/cart/domain/errors&#x27;;
import {
getAuthenticatedUserId,
parseBody,
} from &#x27;@/shared/presentation/route-helpers&#x27;;

/**
* POST /api/cart/checkout/confirm — confirms the checkout.
*
* Spec REQ-CART-030:
* - 201 with { orderIds, total, currency }
* - 401 if unauthenticated
* - 409 if prices changed and user hasn&#x27;t accepted (PriceChangedError)
* - 422 if cart is empty (EmptyCartError)
*
* The `acceptPriceChanges` flag controls whether the checkout proceeds
* when prices have drifted since the user added items. If true, the
* snapshots are updated to current prices and the checkout completes.
* If false, a PriceChangedError is raised (409).
*/
export const POST = requireRole(&#x27;CUSTOMER&#x27;)(async function POST(
request: NextRequest,
) {
const userId = await getAuthenticatedUserId();
if (!userId) {
return NextResponse.json({ error: &#x27;Unauthorized&#x27; }, { status: 401 });
}

try {
const validated = await parseBody(request, confirmCheckoutSchema);</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\cart\checkout\confirm\route.ts (Line 38:5 - Line 54:10),
app\api\cart\checkout\route.ts (Line 28:5 - Line 44:10)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-3')">Show code</button>
</div>
<pre id="code-typescript-3" class="code-block"> const cartRepository = container.getCartRepository();
const productRepository = container.getCartProductRepository();
const outboxRepository = container.getOutboxRepository();
const paidOrderCountPort = container.getPaidOrderCountPort();
const transactionRunner = container.getTransactionRunner();
const customizationLookup = container.getCustomizationLookup();

const checkoutCart = new CheckoutCart(
cartRepository,
productRepository,
outboxRepository,
paidOrderCountPort,
transactionRunner,
customizationLookup,
);

const result = await checkoutCart.confirm(</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\cart\checkout\confirm\route.ts (Line 65:21 - Line 84:4),
app\api\cart\checkout\route.ts (Line 58:21 - Line 77:4)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-4')">Show code</button>
</div>
<pre id="code-typescript-4" class="code-block"> { status: 201 },
);
} catch (error: unknown) {
// PriceChangedError needs special handling: return 409 with priceChanges[]
if (error instanceof PriceChangedError) {
return NextResponse.json(
{
error: error.safeMessage,
priceChanges: error.priceChanges.map((pc) =&gt; ({
itemId: pc.itemId,
oldPrice: pc.oldPrice.amount,
newPrice: pc.newPrice.amount,
})),
},
{ status: 409 },
);
}
return handleApiError(error);
}
});</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\products\[id]\route.ts (Line 16:62 - Line 33:10),
app\api\products\route.ts (Line 73:49 - Line 90:10)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-5')">Show code</button>
</div>
<pre id="code-typescript-5" class="code-block"> const body = productFormSchema.parse(await request.json());
const session = await container.getSession().getSession();

if (!session?.id) {
return NextResponse.json({ error: &#x27;Unauthorized&#x27; }, { status: 401 });
}

const seller = await container
.getSellerRepository()
.findByUserId(session.id);
if (!seller) {
return NextResponse.json(
{ error: &#x27;No seller account found for this user&#x27; },
{ status: 403 },
);
}

const product = await container</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\sellers\[id]\route.ts (Line 45:6 - Line 70:10),
app\api\sellers\[id]\route.ts (Line 94:7 - Line 118:10)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-6')">Show code</button>
</div>
<pre id="code-typescript-6" class="code-block"> req: NextRequest,
context: { params: Promise&lt;{ id: string }&gt; },
) {
try {
const { id } = await context.params;

// Auth check FIRST — 401 must come before any 404
const session = await getSessionUserContext();
if (!session) {
return NextResponse.json({ error: &#x27;Unauthorized&#x27; }, { status: 401 });
}
const userId = session.userId;
const role = session.role;

// Load seller
const sellerRepository = container.getSellerRepository();
const existing = await sellerRepository.findById(id);
if (!existing) {
return NextResponse.json({ error: &#x27;Seller not found&#x27; }, { status: 404 });
}

if (role !== &#x27;ADMIN&#x27; &amp;&amp; userId !== existing.userId) {
return NextResponse.json({ error: &#x27;Forbidden&#x27; }, { status: 403 });
}

const body = updateSellerSchema.parse(await req.json());</pre>
</div>

<div class="clone-item">
<div class="clone-info">
app\api\sellers\[id]\route.ts (Line 83:72 - Line 106:10),
app\api\uploads\[id]\route.ts (Line 43:5 - Line 66:10)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-7')">Show code</button>
</div>
<pre id="code-typescript-7" class="code-block"> return NextResponse.json(toSellerResponse(updated), { status: 200 });
} catch (error: unknown) {
return handleApiError(error);
}
}

/**
* DELETE /api/sellers/[id]
* Admin OR the seller themselves can soft-delete.
*/
export async function DELETE(
_req: NextRequest,
context: { params: Promise&lt;{ id: string }&gt; },
) {
try {
const { id } = await context.params;

// Auth check FIRST — 401 must come before any 404
const session = await getSessionUserContext();
if (!session) {
return NextResponse.json({ error: &#x27;Unauthorized&#x27; }, { status: 401 });
}
const userId = session.userId;
const role = session.role;</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\auth\presentation\schemas\auth-schemas.ts (Line 20:31 - Line 30:4),
modules\auth\presentation\schemas\auth-schemas.ts (Line 72:37 - Line 81:4)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-8')">Show code</button>
</div>
<pre id="code-typescript-8" class="code-block"> confirmPassword: z.string().optional(),
address: z
.object({
street: z.string().min(1),
city: z.string().min(1),
postalCode: z.string().min(1),
country: z.string().min(1),
})
.optional(),
})
.refine(</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\customizations\application\create-customer-customization.ts (Line 110:36 - Line 125:2),
modules\customizations\application\create-customization.ts (Line 56:6 - Line 71:2)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-9')">Show code</button>
</div>
<pre id="code-typescript-9" class="code-block"> CustomizationOptions.create(dto);

const entity: CustomizationEntity = {
id: crypto.randomUUID(),
productId: dto.productId,
text: dto.text ?? null,
color: dto.color ?? null,
size: dto.size ?? null,
imageUrl: dto.imageUrl ?? null,
designPosition: dto.designPosition ?? null,
createdAt: new Date(),
};

return this.repo.save(entity);
}
}</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\users\infrastructure\prisma-user-repository.ts (Line 64:9 - Line 74:9),
modules\users\infrastructure\prisma-user-repository.ts (Line 77:9 - Line 87:9)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-10')">Show code</button>
</div>
<pre id="code-typescript-10" class="code-block"> email: user.email.value,
firstName: user.firstName,
lastName: user.lastName,
passwordHash: user.passwordHash?.value ?? null,
role: user.roleId.value,
addressStreet: user.address?.street ?? null,
addressCity: user.address?.city ?? null,
addressPostalCode: user.address?.postalCode ?? null,
addressCountry: user.address?.country ?? null,
deletedAt: user.deletedAt ?? null,
},</pre>
</div>

<div class="clone-item">
<div class="clone-info">
modules\users\infrastructure\prisma-user-repository.ts (Line 79:32 - Line 90:21),
modules\users\infrastructure\prisma-user-repository.ts (Line 127:32 - Line 138:21)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-11')">Show code</button>
</div>
<pre id="code-typescript-11" class="code-block"> lastName: user.lastName,
passwordHash: user.passwordHash?.value ?? null,
role: user.roleId.value,
addressStreet: user.address?.street ?? null,
addressCity: user.address?.city ?? null,
addressPostalCode: user.address?.postalCode ?? null,
addressCountry: user.address?.country ?? null,
deletedAt: user.deletedAt ?? null,
},
});

return toDomain(savedUser);</pre>
</div>

<div class="clone-item">
<div class="clone-info">
tests\doubles\memory-customization-lookup.ts (Line 31:17 - Line 63:2),
tests\doubles\memory-order-customization-lookup.ts (Line 23:18 - Line 53:2)
</div>
<div>
<button class="toggle-btn" onclick="toggle('code-typescript-12')">Show code</button>
</div>
<pre id="code-typescript-12" class="code-block"> .toArray();
}

/** Seed customizations in plain-JSON form for readability in tests. */
seed(
customizations: Array&lt;{
id: string;
productId: string;
text?: string | null;
color?: string | null;
size?: string | null;
imageUrl?: string | null;
designPosition?: CustomizationDesignPositionSnapshot | null;
}&gt;,
): void {
for (const c of customizations) {
this.customizations.set(c.id, {
id: c.id,
productId: c.productId,
text: c.text ?? null,
color: c.color ?? null,
size: c.size ?? null,
imageUrl: c.imageUrl ?? null,
designPosition: c.designPosition ?? null,
});
}
}

/** Test helper — clear all stored customizations between tests. */
clear(): void {
this.customizations.clear();
}
}</pre>
</div>

</div>
</section>
</div>


</div>

<div class="footer">
<p>Generated by <a href="https://jscpd.dev" target="_blank">jscpd</a> v0.1.7 by <a href="https://github.com/kucherenko" target="_blank">Andrey Kucherenko</a></p>
<p><a href="https://www.npmjs.com/package/jscpd" target="_blank"><img src="https://img.shields.io/npm/dw/jscpd.svg?style=flat-square" alt="npm weekly downloads" style="vertical-align: middle;"></a></p>
</div>

<script>
function toggle(id) {
var el = document.getElementById(id);
if (el) { el.classList.toggle('visible'); }
}
</script>
</body>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Reformatea este reporte generado.

La CI ya está fallando por Prettier en este archivo. Regénéralo o aplícale el formateo esperado antes de mergear.

🧰 Tools
🪛 GitHub Actions: CI / 0_quality.txt

[warning] 1-1: Prettier check reported code style issues in this file. Run 'prettier . --write' to fix.

🪛 GitHub Actions: CI / quality

[warning] 1-1: Prettier check (prettier . --check) reported code style issues in this file. Run 'prettier --write' to fix.

🤖 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 `@report/jscpd-report.html` around lines 1 - 691, The generated jscpd report
HTML needs to be reformatted to satisfy Prettier before merge. Re-run the report
generator or apply the repo’s expected formatting to the full document, keeping
the structure and content in report/jscpd-report.html intact while making it
Prettier-compliant. Focus on the overall HTML/CSS/JS in the report output rather
than changing the report data.

Source: Pipeline failures

Comment on lines +10 to +18
export async function getSessionUserContext(): Promise<SessionUserContext | null> {
const session = await getServerSession(authOptions);
if (!session) return null;
const user = session.user as SessionUserContext | undefined;
return {
userId: user?.userId ?? user?.id,
role: user?.role,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C2 'getSessionUserContext\s*\('

Repository: henri318/728-store

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files | rg 'shared/authorization/session-user-context\.ts|auth-options|route\.ts|DeleteUploadUseCase|uploads/.*/route\.ts'

printf '\n== session-user-context ==\n'
sed -n '1,120p' shared/authorization/session-user-context.ts

printf '\n== auth-options matches ==\n'
rg -n --glob '*.ts' 'session\.user\.id|userId|role|callbacks:|jwt\(' shared app src

printf '\n== getSessionUserContext consumers ==\n'
rg -n --glob '*.ts' 'getSessionUserContext\s*\(' .

printf '\n== upload route context ==\n'
sed -n '1,180p' app/api/uploads/'[id]'/route.ts

Repository: henri318/728-store

Length of output: 11865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== auth-options relevant slice ==\n'
sed -n '150,190p' shared/infrastructure/auth-options.ts

printf '\n== upload route relevant slice ==\n'
sed -n '1,130p' app/api/uploads/'[id]'/route.ts

printf '\n== other getSessionUserContext usages ==\n'
rg -n --glob '*.ts' 'getSessionUserContext\s*\(' app shared

Repository: henri318/728-store

Length of output: 4389


Haz que getSessionUserContext() falle cerrado sin userId

En shared/authorization/session-user-context.ts, una sesión puede existir con session.user.id === '' (el callback de sesión usa token.id ?? ''), así que if (!session) no garantiza un usuario identificable. Devuelve null cuando no haya userId resoluble; así app/api/uploads/[id]/route.ts no llega a deleteUpload.execute(id, userId!, isAdmin).

🤖 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/authorization/session-user-context.ts` around lines 10 - 18,
getSessionUserContext() currently returns a context even when session.user has
no resolvable userId, which can let downstream code proceed with an empty
identifier. Update getSessionUserContext in session-user-context.ts to fail
closed by checking the resolved userId after reading session.user and returning
null unless it is present and non-empty; keep the existing SessionUserContext
shape otherwise so callers like the uploads route never reach
deleteUpload.execute with an invalid userId.

Comment on lines +35 to +54
export function drawDesign(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
position: { x: number; y: number; scale: number; rotation_deg: number },
width: number,
height: number,
) {
const scaleFactor = position.scale / 100;
const baseWidth = Math.min(width, image.naturalWidth);
const ratio = image.naturalHeight / image.naturalWidth;
const drawWidth = baseWidth * scaleFactor;
const drawHeight = drawWidth * ratio;

const centerX = position.x * width;
const centerY = position.y * height;

ctx.translate(centerX, centerY);
ctx.rotate((position.rotation_deg * Math.PI) / 180);
ctx.drawImage(image, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirmar qué llamadores envuelven drawDesign en save/restore
rg -nP -C6 'drawDesign\(' -g '*.tsx' -g '*.ts'

Repository: henri318/728-store

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== drawDesign usages =="
rg -n -C 4 'drawDesign\s*\(' .

echo
echo "== candidate canvas-related files =="
git ls-files | rg 'shared/presentation/(canvas-utils|design-preview|mockup|canvas).*\.tsx?$'

Repository: henri318/728-store

Length of output: 2513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== design-preview.tsx =="
cat -n modules/presentation/components/design-preview.tsx | sed -n '1,140p'

echo
echo "== mockup-canvas-control.tsx =="
cat -n app/[locale]/products/[id]/mockup-canvas-control.tsx | sed -n '110,170p'

Repository: henri318/728-store

Length of output: 4946


drawDesign debe aislar su transformación del contexto.

drawDesign aplica translate/rotate sobre ctx pero no restaura el estado. En modules/presentation/components/design-preview.tsx se llama sin save/restore, así que el contexto queda transformado entre renders y los siguientes repintados pueden salir desplazados o rotados. Encapsúlalo en ctx.save()/ctx.restore() dentro de drawDesign.

🤖 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/canvas-utils.ts` around lines 35 - 54, drawDesign is
mutating the canvas context without restoring it, so subsequent renders can
inherit the translate/rotate state. Update drawDesign in canvas-utils.ts to wrap
its transform and drawImage calls with ctx.save() and ctx.restore() inside the
function, using the drawDesign symbol to isolate the context state even when it
is called directly from design-preview.tsx without external save/restore.

Comment on lines +25 to +30
export async function getCurrentSellerId(): Promise<string | null> {
const userId = await getAuthenticatedUserId();
if (!userId) return null;
const seller = await container.getSellerRepository().findByUserId(userId);
return seller?.sellerId.value ?? null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

getCurrentSellerId conflaciona "no autenticado" con "no es seller".

Si userId es null (usuario anónimo) o si el usuario autenticado no tiene un seller asociado, ambos casos retornan null. Los consumidores (p. ej. app/api/customizations/route.ts, app/api/customizations/[id]/route.ts) tratan cualquier null como 403 Forbidden, incluso cuando el request es realmente anónimo y debería ser 401 Unauthorized. Esto puede confundir a clientes de la API y a herramientas de observabilidad que dependen de los códigos de estado.

Considera devolver un resultado que distinga ambos casos (p. ej. lanzando o retornando un sentinel distinto) para que los callers puedan mapear correctamente 401 vs 403.

🤖 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/route-helpers.ts` around lines 25 - 30,
`getCurrentSellerId` is conflating anonymous access with an authenticated user
who has no seller record. Update `getCurrentSellerId` in `route-helpers.ts` to
return a distinguishable result for “not authenticated” versus “authenticated
but no seller,” rather than always `null`. Then adjust the callers in the
customization route handlers to map anonymous requests to 401 Unauthorized and
authenticated non-seller requests to 403 Forbidden, using the new return
shape/sentinel consistently.

Comment thread shared/ui/input.tsx
Comment thread shared/ui/text-field.tsx
@henri318 henri318 merged commit 2a20b0f into main Jul 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant