2026-07-13 18:05:40 -04:00
|
|
|
// Customers domain use-case hooks: create/update must invalidate the list
|
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
|
|
|
// whole domain key family (customersKeys.all — lists AND cached search
|
|
|
|
|
// autocompletes; code-review finding, Phase 4) and toast. The domain api module is
|
2026-07-13 18:05:40 -04:00
|
|
|
// mocked — no axios traffic.
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
|
|
|
import { toast } from 'react-toastify';
|
|
|
|
|
import { createQueryHarness } from './hookTestUtils';
|
|
|
|
|
import {
|
|
|
|
|
customersKeys,
|
|
|
|
|
useCreateCustomer,
|
|
|
|
|
useUpdateCustomer,
|
|
|
|
|
} from '../customers/use-cases';
|
|
|
|
|
import { customersApi } from '../customers/api';
|
|
|
|
|
import type { Customer } from '../customers/types';
|
|
|
|
|
|
|
|
|
|
vi.mock('../customers/api', () => ({
|
|
|
|
|
customersApi: {
|
|
|
|
|
search: vi.fn(),
|
|
|
|
|
list: vi.fn(),
|
|
|
|
|
create: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('react-toastify', () => ({
|
|
|
|
|
toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const customer = { id: 'c-1', name: 'Acme Corp' } as unknown as Customer;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('customersKeys', () => {
|
|
|
|
|
it('list keys nest under lists() so one invalidation hits every page/filter', () => {
|
|
|
|
|
expect(customersKeys.all).toEqual(['customers']);
|
|
|
|
|
expect(customersKeys.lists()).toEqual(['customers', 'list']);
|
|
|
|
|
expect(customersKeys.list({ page: 3 })).toEqual(['customers', 'list', { page: 3 }]);
|
|
|
|
|
expect(customersKeys.search('acme')).toEqual(['customers', 'search', 'acme']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('useCreateCustomer', () => {
|
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
|
|
|
it('invalidates the whole customers domain (lists + search caches) and toasts on success', async () => {
|
2026-07-13 18:05:40 -04:00
|
|
|
vi.mocked(customersApi.create).mockResolvedValue(customer);
|
|
|
|
|
const { queryClient, wrapper } = createQueryHarness();
|
|
|
|
|
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useCreateCustomer(), { wrapper });
|
|
|
|
|
result.current.mutate({ name: 'Acme Corp' } as never);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
|
|
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: customersKeys.all });
|
2026-07-13 18:05:40 -04:00
|
|
|
expect(toast.success).toHaveBeenCalledWith('Customer created');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toasts the failure and skips invalidation', async () => {
|
|
|
|
|
vi.mocked(customersApi.create).mockRejectedValue(new Error('duplicate name'));
|
|
|
|
|
const { queryClient, wrapper } = createQueryHarness();
|
|
|
|
|
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useCreateCustomer(), { wrapper });
|
|
|
|
|
result.current.mutate({ name: 'Acme Corp' } as never);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
|
expect(toast.error).toHaveBeenCalledWith('Failed to create customer: duplicate name');
|
|
|
|
|
expect(invalidateSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('useUpdateCustomer', () => {
|
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
|
|
|
it('passes id + payload through and invalidates the whole customers domain on success', async () => {
|
2026-07-13 18:05:40 -04:00
|
|
|
vi.mocked(customersApi.update).mockResolvedValue(customer);
|
|
|
|
|
const { queryClient, wrapper } = createQueryHarness();
|
|
|
|
|
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useUpdateCustomer(), { wrapper });
|
|
|
|
|
const data = { name: 'Acme Corp (Renamed)' } as never;
|
|
|
|
|
result.current.mutate({ id: 'c-1', data });
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
|
expect(customersApi.update).toHaveBeenCalledWith('c-1', data);
|
fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
2026-07-13 19:06:08 -04:00
|
|
|
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: customersKeys.all });
|
2026-07-13 18:05:40 -04:00
|
|
|
expect(toast.success).toHaveBeenCalledWith('Customer updated');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toasts the failure on update error', async () => {
|
|
|
|
|
vi.mocked(customersApi.update).mockRejectedValue(new Error('not found'));
|
|
|
|
|
const { wrapper } = createQueryHarness();
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() => useUpdateCustomer(), { wrapper });
|
|
|
|
|
result.current.mutate({ id: 'c-404', data: { name: 'x' } as never });
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
|
expect(toast.error).toHaveBeenCalledWith('Failed to update customer: not found');
|
|
|
|
|
});
|
|
|
|
|
});
|