Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/features/inventory/hooks/useInventory.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
15 changes: 14 additions & 1 deletion src/features/inventory/hooks/useInventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product>({
return useQuery<Product, ApiRequestError>({
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,
})
}

Expand Down
22 changes: 22 additions & 0 deletions src/features/inventory/pages/ProductDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})
})
14 changes: 12 additions & 2 deletions src/features/inventory/pages/ProductDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (
<PageWrapper>
<Stack spacing={2} alignItems="flex-start">
Expand Down
Loading