diff --git a/lib/services/phishing-ticket-resolver.ts b/lib/services/phishing-ticket-resolver.ts new file mode 100644 index 0000000..eee95a5 --- /dev/null +++ b/lib/services/phishing-ticket-resolver.ts @@ -0,0 +1,48 @@ +/** + * Phishing Ticket -> Campaign Resolver (Phase 22, Wave 0) + * + * Pure lookup service: given an Autotask ticket id, finds the linked + * `reports` row (if any) and reports back its campaign linkage. No auth, + * no param parsing — that lives in the route (plan 02, GET + * /api/phishing/tickets/[ticket_id]/campaign). Extracted as its own module + * so it is unit-testable under vitest (whose `test.include` is + * `lib/**\/*.test.ts` only — `app/**` route files have no coverage). + */ + +import { postgresClient } from './postgres-client'; + +export interface TicketCampaignResolution { + found: boolean; + reportId?: string; + campaignId?: string | null; + ticketNumber?: string | null; +} + +interface ReportLookupRow { + id: string; + campaign_id: string | null; + ticket_number: string | null; +} + +/** + * Looks up the `reports` row for a given ticket id (tickets.id / reports.ticket_id). + * - No row: `{ found: false }` + * - Row with `campaign_id = null`: `{ found: true, reportId, campaignId: null, ticketNumber }` — ungrouped report + * - Row with a `campaign_id`: `{ found: true, reportId, campaignId, ticketNumber }` — grouped into a campaign + */ +export async function resolveTicketToCampaign(ticketId: number): Promise { + const res = await postgresClient.query( + `SELECT id::text, campaign_id::text, ticket_number FROM reports WHERE ticket_id = $1`, + [ticketId] + ); + const report = res.rows[0]; + if (!report) { + return { found: false }; + } + return { + found: true, + reportId: report.id, + campaignId: report.campaign_id, + ticketNumber: report.ticket_number, + }; +}