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.
86 lines
2.7 KiB
TypeScript
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 };
|
|
}
|