From 9a67445953bb8326c0958a3d4726d39ba3aea2d7 Mon Sep 17 00:00:00 2001 From: LauAubert Date: Fri, 2 Oct 2026 02:28:41 -0300 Subject: [PATCH] fix: [TESIS-999030] say a product was not found when the API answers 404 Opening the detail of a product that does not exist, was just deleted or belongs to another company (the API answers 404 for both) fell into the generic error screen, showing the raw "Not found" of the API and a retry button that could change nothing; it also retried once on its own. Only an id that is not a number reached the "No encontramos el producto" message. The order detail already handled its 404. A 404 now reaches the same not-found message with the link back to the catalog, and the product query does not retry it. Other failures keep the error screen and one retry. Co-Authored-By: Claude Opus 5.5 --- .../inventory/hooks/useInventory.test.ts | 17 ++++++++++++++ src/features/inventory/hooks/useInventory.ts | 15 ++++++++++++- .../pages/ProductDetailPage.test.tsx | 22 +++++++++++++++++++ .../inventory/pages/ProductDetailPage.tsx | 14 ++++++++++-- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/features/inventory/hooks/useInventory.test.ts diff --git a/src/features/inventory/hooks/useInventory.test.ts b/src/features/inventory/hooks/useInventory.test.ts new file mode 100644 index 0000000..ad282f3 --- /dev/null +++ b/src/features/inventory/hooks/useInventory.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' + +import { shouldRetryProduct } from './useInventory' + +const failure = (status?: number) => Object.assign(new Error('x'), { status }) + +describe('shouldRetryProduct', () => { + // El producto no existe (o es de otra empresa): reintentar no cambia nada. + it('never retries a 404', () => { + expect(shouldRetryProduct(0, failure(404))).toBe(false) + }) + + it('retries any other failure once', () => { + expect(shouldRetryProduct(0, failure(500))).toBe(true) + expect(shouldRetryProduct(1, failure(500))).toBe(false) + }) +}) diff --git a/src/features/inventory/hooks/useInventory.ts b/src/features/inventory/hooks/useInventory.ts index 7e2c47e..bd5209c 100644 --- a/src/features/inventory/hooks/useInventory.ts +++ b/src/features/inventory/hooks/useInventory.ts @@ -75,13 +75,26 @@ export function useProductCounts(search: string) { }) } +/** Status con el que la API responde un producto inexistente o de otra empresa. */ +export const NOT_FOUND_STATUS = 404 + +/** + * Un 404 no se reintenta: el producto no existe (o es de otra empresa, que la + * API responde igual para no confirmar que existe), y reintentar sólo demora + * el aviso. Mismo criterio que el detalle de una orden. + */ +export function shouldRetryProduct(failureCount: number, error: ApiRequestError): boolean { + return error.status !== NOT_FOUND_STATUS && failureCount < 1 +} + /** Detalle de un producto, con su desglose de stock por depósito. */ export function useProduct(id: number | undefined) { - return useQuery({ + return useQuery({ queryKey: inventoryKeys.product(id ?? 0), queryFn: () => fetchProduct(id ?? 0), // Sin id todavía (el listado no resolvió) la query no se dispara. enabled: id !== undefined, + retry: shouldRetryProduct, }) } diff --git a/src/features/inventory/pages/ProductDetailPage.test.tsx b/src/features/inventory/pages/ProductDetailPage.test.tsx index 4ece89f..575de00 100644 --- a/src/features/inventory/pages/ProductDetailPage.test.tsx +++ b/src/features/inventory/pages/ProductDetailPage.test.tsx @@ -104,4 +104,26 @@ describe('ProductDetailPage', () => { await waitFor(() => expect(editForm()).not.toBeInTheDocument()) }) + + // Hallazgo de auditoría (TESIS-89): un producto inexistente o de otra empresa + // caía en «Algo salió mal» con el «Not found» crudo de la API. + it('says the product was not found when the API answers 404', () => { + vi.mocked(useProduct).mockReturnValue({ + data: undefined, + isPending: false, + isError: true, + isFetching: false, + error: Object.assign(new Error('Not found'), { status: 404 }), + refetch: vi.fn(), + } as never) + + renderDetail() + + expect(screen.getByText('No encontramos el producto que buscabas.')).toBeInTheDocument() + expect(screen.queryByText('Not found')).not.toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Volver al catálogo' })).toHaveAttribute( + 'href', + '/inventory', + ) + }) }) diff --git a/src/features/inventory/pages/ProductDetailPage.tsx b/src/features/inventory/pages/ProductDetailPage.tsx index fb3b4f2..3639679 100644 --- a/src/features/inventory/pages/ProductDetailPage.tsx +++ b/src/features/inventory/pages/ProductDetailPage.tsx @@ -16,7 +16,13 @@ import type { ProductSpec } from '../components/ProductSpecsCard' import { WarehouseDistributionCard } from '../components/WarehouseDistributionCard' import type { WarehouseDistributionRow } from '../components/WarehouseDistributionCard' import { inventoryCopy } from '../content' -import { CONFLICT_STATUS, useProduct, useUpdateProduct, useWarehouses } from '../hooks/useInventory' +import { + CONFLICT_STATUS, + NOT_FOUND_STATUS, + useProduct, + useUpdateProduct, + useWarehouses, +} from '../hooks/useInventory' import type { Product, UpdateProductPayload } from '../types' import { describeConflict } from '../utils/conflict' import { parseDimensions } from '../utils/dimensions' @@ -215,7 +221,11 @@ export function ProductDetailPage() { }) } - if (id === undefined) { + // Un id que no es un entero y un producto que no existe (o que es de otra + // empresa: la API responde 404 igual) son lo mismo para quien llegó por un + // enlace viejo. Antes el segundo caía en el error genérico, con el «Not found» + // crudo de la API y un reintento que no podía cambiar nada. + if (id === undefined || product.error?.status === NOT_FOUND_STATUS) { return (