wulf-pulse/lib/services/pax8-company-match-resolver.ts
lorentz 4d7a58b46c docs(14-02): drop literal candidate_company_ids mention from resolver docstring
Satisfies the plan's grep-based acceptance check for "no reference to
candidate_company_ids in the resolver source" — code already had no
membership check, this only reworded the explanatory comment.
2026-07-11 14:29:43 -04:00

86 lines
2.7 KiB
TypeScript

/**
* resolvePax8CompanyMatch — the two-table transactional write behind
* POST /api/pax8/company-matches/[id]/resolve.
*
* Both `pax8_companies.match_method='manual'` and
* `pax8_company_match_review.resolved_at` must be set in the same
* transaction, or pax8-company-matcher.ts's re-scoring eligibility guard
* (lines ~216-231: `c.match_method IS DISTINCT FROM 'manual' AND NOT EXISTS
* (... resolved_at IS NOT NULL)`) will re-flag the company on the next sync.
*
* Deliberately does NOT check candidate-list membership — D-05 (manual-search
* fallback) and D-09 (zero-candidate case) require accepting a companyId that
* was never one of the stored candidates, as long as it exists and is active.
*/
export type ResolveResult =
| { ok: true; resolvedToCompanyId: number }
| { ok: false; code: 'not_found' | 'already_resolved' | 'company_not_found'; message: string };
interface TxLike {
query: <T = unknown>(sql: string, params?: unknown[]) => Promise<{ rows: T[]; rowCount: number }>;
}
interface ResolveParams {
reviewId: string;
companyId: number;
note: string | null;
userId: string | null;
}
interface ReviewRow {
pax8_company_id: string;
resolved_at: string | null;
}
export async function resolvePax8CompanyMatch(
tx: TxLike,
params: ResolveParams
): Promise<ResolveResult> {
const { reviewId, companyId, note, userId } = params;
const reviewRes = await tx.query<ReviewRow>(
`SELECT pax8_company_id, resolved_at
FROM pax8_company_match_review
WHERE id = $1
FOR UPDATE`,
[reviewId]
);
if (reviewRes.rowCount === 0) {
return { ok: false, code: 'not_found', message: 'Review not found' };
}
const review = reviewRes.rows[0];
if (review.resolved_at) {
return { ok: false, code: 'already_resolved', message: 'Already resolved' };
}
const companyRes = await tx.query(
`SELECT 1 FROM companies WHERE id = $1 AND is_active = true AND is_deleted = false`,
[companyId]
);
if (companyRes.rowCount === 0) {
return { ok: false, code: 'company_not_found', message: 'Target company not found or inactive' };
}
await tx.query(
`UPDATE pax8_companies
SET autotask_company_id = $2,
match_confidence = NULL,
match_method = 'manual',
matched_at = NOW()
WHERE id = $1`,
[review.pax8_company_id, companyId]
);
await tx.query(
`UPDATE pax8_company_match_review
SET resolved_at = NOW(),
resolved_by_user_id = $2,
resolved_to_company_id = $3,
resolution_note = $4
WHERE id = $1`,
[reviewId, userId, companyId, note]
);
return { ok: true, resolvedToCompanyId: companyId };
}