diff --git a/src/features/orders/hooks/useUpdateOrder.test.tsx b/src/features/orders/hooks/useUpdateOrder.test.tsx
new file mode 100644
index 0000000..8e2f07c
--- /dev/null
+++ b/src/features/orders/hooks/useUpdateOrder.test.tsx
@@ -0,0 +1,68 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { act, renderHook } from '@testing-library/react'
+import type { ReactNode } from 'react'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import * as api from '../api'
+import { orderKeys, quoteKeys } from '../queryKeys'
+import type { OrderDetail, UpdateOrderPayload } from '../types'
+
+import { useUpdateOrder } from './useUpdateOrder'
+
+const PAYLOAD = { order: { status: 'paid' } } as unknown as UpdateOrderPayload
+
+// El detalle, la cotización de esta orden y la de otra, ya en caché.
+function seededClient() {
+ const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
+ queryClient.setQueryData(orderKeys.detail(8829), {})
+ queryClient.setQueryData(quoteKeys.order(8829, 1), [{ shippingCost: 2500 }])
+ queryClient.setQueryData(quoteKeys.order(7000, 1), [{ shippingCost: 900 }])
+ return queryClient
+}
+
+function renderFor(queryClient: QueryClient) {
+ return renderHook(() => useUpdateOrder(8829, '"v1"'), {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ })
+}
+
+beforeEach(() => {
+ vi.restoreAllMocks()
+})
+
+describe('useUpdateOrder', () => {
+ // Hallazgo de auditoría (TESIS-89): la cotización se calculó con las líneas y
+ // el destino de antes, y despachar dentro del minuto de caché mandaba el
+ // costo viejo.
+ it('drops the shipping quote of the order it saved', async () => {
+ vi.spyOn(api, 'updateOrder').mockResolvedValue({} as OrderDetail)
+ const queryClient = seededClient()
+ const { result } = renderFor(queryClient)
+
+ await act(() => result.current.mutateAsync(PAYLOAD))
+
+ expect(queryClient.getQueryData(quoteKeys.order(8829, 1))).toBeUndefined()
+ })
+
+ it('keeps the quotes of other orders', async () => {
+ vi.spyOn(api, 'updateOrder').mockResolvedValue({} as OrderDetail)
+ const queryClient = seededClient()
+ const { result } = renderFor(queryClient)
+
+ await act(() => result.current.mutateAsync(PAYLOAD))
+
+ expect(queryClient.getQueryData(quoteKeys.order(7000, 1))).toEqual([{ shippingCost: 900 }])
+ })
+
+ it('still refreshes the order', async () => {
+ vi.spyOn(api, 'updateOrder').mockResolvedValue({} as OrderDetail)
+ const queryClient = seededClient()
+ const { result } = renderFor(queryClient)
+
+ await act(() => result.current.mutateAsync(PAYLOAD))
+
+ expect(queryClient.getQueryState(orderKeys.detail(8829))?.isInvalidated).toBe(true)
+ })
+})
diff --git a/src/features/orders/hooks/useUpdateOrder.ts b/src/features/orders/hooks/useUpdateOrder.ts
index e07b5c4..02f759d 100644
--- a/src/features/orders/hooks/useUpdateOrder.ts
+++ b/src/features/orders/hooks/useUpdateOrder.ts
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { ApiRequestError } from 'shared/api/types'
import { updateOrder } from '../api'
-import { orderKeys } from '../queryKeys'
+import { orderKeys, quoteKeys } from '../queryKeys'
import type { OrderDetail, UpdateOrderPayload } from '../types'
/** La API rechaza con 412 el guardado que parte de una versión vieja. */
@@ -17,17 +17,26 @@ export const NOT_EDITABLE_STATUS = 409
* Al terminar invalida el dominio entero de órdenes —el listado muestra el total
* y el destino— y el catálogo: cambiar las líneas mueve stock, así que el
* `total_stock` del inventario y el stock por depósito también quedaron viejos.
+ * Y borra la cotización de la orden, que dependía de esas líneas y ese destino.
*/
export function useUpdateOrder(id: number, version: string | null) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (payload) => updateOrder(id, payload, version),
- onSuccess: () =>
- Promise.all([
+ onSuccess: () => {
+ // La cotización de esta orden se calculó con las líneas y el destino de
+ // antes: dura un minuto en caché, y despachar dentro de ese minuto mandaba
+ // el costo viejo (hallazgo de auditoría, TESIS-89). Se borra y no sólo se
+ // invalida: el diálogo de despacho está cerrado, y una entrada invalidada
+ // se seguiría mostrando hasta que vuelva la nueva.
+ queryClient.removeQueries({ queryKey: [...quoteKeys.all, 'order', id] })
+
+ return Promise.all([
queryClient.invalidateQueries({ queryKey: orderKeys.all }),
// Literal y no la factory de inventario: una feature no importa otra.
queryClient.invalidateQueries({ queryKey: ['inventory'] }),
- ]),
+ ])
+ },
})
}