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 (