From e7d8911b696c26267424d997b6418b4d13120660 Mon Sep 17 00:00:00 2001 From: LauAubert Date: Fri, 2 Oct 2026 02:20:39 -0300 Subject: [PATCH] fix: [TESIS-999026] drop the shipping quote of an order when it is modified The quote of an existing order is cached for a minute and its key only holds the order and the origin warehouse. The API quotes with the current lines and destination of the order, but saving a modification invalidated orders and inventory only: dispatching within that minute showed the old quote and sent its shipping_cost, which the backend stores on the shipment. Saving a modification now removes the quotes of that order, so the next dispatch asks the carriers again with the new lines and address. Quotes of other orders are left alone. Co-Authored-By: Claude Opus 5.5 --- .../orders/hooks/useUpdateOrder.test.tsx | 68 +++++++++++++++++++ src/features/orders/hooks/useUpdateOrder.ts | 17 +++-- 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/features/orders/hooks/useUpdateOrder.test.tsx 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'] }), - ]), + ]) + }, }) }