- 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
162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
/**
|
|
* 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<typeof vi.fn<(...args: any[]) => Promise<{ rows: unknown[]; rowCount: number }>>>;
|
|
|
|
function makeTx(queryMock: QueryMock) {
|
|
return {
|
|
query: <T = unknown>(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 });
|
|
});
|
|
});
|