From afdcf1412dfb8034195408fe54232a908d5c1fc7 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 14:28:44 -0400 Subject: [PATCH] test(14-02): add failing test for resolvePax8CompanyMatch - Five behavior cases: success (both writes), not_found, already_resolved, company_not_found, and non-candidate companyId still resolves (D-05/D-09) - Hand-rolled mock tx asserts SQL + bound params per query call --- .../pax8-company-match-resolver.test.ts | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 lib/services/pax8-company-match-resolver.test.ts diff --git a/lib/services/pax8-company-match-resolver.test.ts b/lib/services/pax8-company-match-resolver.test.ts new file mode 100644 index 0000000..47fd07b --- /dev/null +++ b/lib/services/pax8-company-match-resolver.test.ts @@ -0,0 +1,162 @@ +/** + * pax8-company-match-resolver.ts unit tests. + * + * The resolver takes `tx` as a parameter (it runs inside + * postgresClient.transaction() at the call site), so no module mock is + * needed here — a hand-rolled mock tx `{ query: vi.fn() }` is scripted per + * test to return the sequenced results, and assertions check the SQL + * strings + bound params of each `query` call plus the returned + * ResolveResult, matching the mocking discipline of + * pax8-company-matcher.test.ts. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { resolvePax8CompanyMatch } from './pax8-company-match-resolver'; + +interface MockCall { + sql: string; + params: unknown[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type QueryMock = ReturnType Promise<{ rows: unknown[]; rowCount: number }>>>; + +function makeTx(queryMock: QueryMock) { + return { + query: (sql: string, params?: unknown[]) => + queryMock(sql, params) as Promise<{ rows: T[]; rowCount: number }>, + }; +} + +function calls(queryMock: QueryMock): MockCall[] { + return queryMock.mock.calls.map(([sql, params]) => ({ + sql: String(sql), + params: (params as unknown[]) ?? [], + })); +} + +describe('resolvePax8CompanyMatch', () => { + it('writes both pax8_companies and pax8_company_match_review on success', async () => { + const queryMock = vi.fn(); + // 1. review SELECT ... FOR UPDATE + queryMock.mockResolvedValueOnce({ + rows: [{ pax8_company_id: 'pax8-uuid-1', resolved_at: null }], + rowCount: 1, + }); + // 2. company existence/active check + queryMock.mockResolvedValueOnce({ rows: [{ '?column?': 1 }], rowCount: 1 }); + // 3. UPDATE pax8_companies + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); + // 4. UPDATE pax8_company_match_review + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); + + const tx = makeTx(queryMock); + const result = await resolvePax8CompanyMatch(tx, { + reviewId: 'review-uuid-1', + companyId: 42, + note: 'looks right', + userId: 'user-1', + }); + + expect(result).toEqual({ ok: true, resolvedToCompanyId: 42 }); + + const c = calls(queryMock); + expect(c).toHaveLength(4); + expect(c[2].sql).toMatch(/UPDATE\s+pax8_companies/i); + expect(c[2].sql).toMatch(/match_method\s*=\s*'manual'/i); + expect(c[2].params).toEqual([ 'pax8-uuid-1', 42 ]); + expect(c[3].sql).toMatch(/UPDATE\s+pax8_company_match_review/i); + expect(c[3].sql).toMatch(/resolved_at\s*=\s*NOW\(\)/i); + expect(c[3].params).toEqual(['review-uuid-1', 'user-1', 42, 'looks right']); + }); + + it('returns not_found and issues no updates when the review row is missing', async () => { + const queryMock = vi.fn(); + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 0 }); + + const tx = makeTx(queryMock); + const result = await resolvePax8CompanyMatch(tx, { + reviewId: 'missing-review', + companyId: 42, + note: null, + userId: 'user-1', + }); + + expect(result).toEqual({ + ok: false, + code: 'not_found', + message: expect.any(String), + }); + expect(queryMock).toHaveBeenCalledTimes(1); + }); + + it('returns already_resolved and issues no updates when resolved_at is set', async () => { + const queryMock = vi.fn(); + queryMock.mockResolvedValueOnce({ + rows: [{ pax8_company_id: 'pax8-uuid-1', resolved_at: '2026-01-01T00:00:00.000Z' }], + rowCount: 1, + }); + + const tx = makeTx(queryMock); + const result = await resolvePax8CompanyMatch(tx, { + reviewId: 'review-uuid-1', + companyId: 42, + note: null, + userId: 'user-1', + }); + + expect(result).toEqual({ + ok: false, + code: 'already_resolved', + message: expect.any(String), + }); + expect(queryMock).toHaveBeenCalledTimes(1); + }); + + it('returns company_not_found and issues no pax8_companies write when target does not exist/is inactive', async () => { + const queryMock = vi.fn(); + queryMock.mockResolvedValueOnce({ + rows: [{ pax8_company_id: 'pax8-uuid-1', resolved_at: null }], + rowCount: 1, + }); + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 0 }); + + const tx = makeTx(queryMock); + const result = await resolvePax8CompanyMatch(tx, { + reviewId: 'review-uuid-1', + companyId: 999, + note: null, + userId: 'user-1', + }); + + expect(result).toEqual({ + ok: false, + code: 'company_not_found', + message: expect.any(String), + }); + expect(queryMock).toHaveBeenCalledTimes(2); + }); + + it('resolves successfully with a companyId NOT in candidate_company_ids (manual-search / zero-candidate case)', async () => { + const queryMock = vi.fn(); + queryMock.mockResolvedValueOnce({ + rows: [{ pax8_company_id: 'pax8-uuid-1', resolved_at: null }], + rowCount: 1, + }); + queryMock.mockResolvedValueOnce({ rows: [{ '?column?': 1 }], rowCount: 1 }); + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); + queryMock.mockResolvedValueOnce({ rows: [], rowCount: 1 }); + + const tx = makeTx(queryMock); + // companyId 777 was never a member of candidate_company_ids for this + // review — resolver must not check membership at all. + const result = await resolvePax8CompanyMatch(tx, { + reviewId: 'review-uuid-1', + companyId: 777, + note: null, + userId: 'user-1', + }); + + expect(result).toEqual({ ok: true, resolvedToCompanyId: 777 }); + }); +});