---
phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve
plan: 01
subsystem: api
tags: [vitest, tdd, postgres, phishing-triage, pure-functions]

# Dependency graph
requires:
  - phase: 19-phishing-triage-classification-engine
    provides: campaign-classifier.ts's mapVerdictToActions 7-action vocabulary and postgresClient/vi.mock testing idiom
  - phase: 20-remediation-approval-audit-safety
    provides: remediation-service.ts's ApproveActionInput shape and phishing-audit.ts's canonical event_type values
provides:
  - resolveTicketToCampaign(ticketId) — pure ticket->campaign lookup service
  - deriveDefaultParams(actionType, evidence) — 7-action-type default-param derivation
  - mergeTimeline(reports, classifications, auditEvents) — chronological 3-source merge
affects: [22-02-ticket-campaign-route, 22-04-action-area-card, 22-05-timeline-card]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Pure lib/services/*.ts extraction for route/component logic that needs vitest coverage (test.include is lib/**/*.test.ts only)"
    - "vi.mock('./postgres-client', () => ({ postgresClient: { query: ... } })) factory-mock idiom (mirrors campaign-classifier.test.ts)"

key-files:
  created:
    - lib/services/phishing-ticket-resolver.ts
    - lib/services/phishing-ticket-resolver.test.ts
    - lib/services/remediation-default-params.ts
    - lib/services/remediation-default-params.test.ts
    - lib/services/phishing-timeline.ts
    - lib/services/phishing-timeline.test.ts
  modified: []

key-decisions:
  - "Used named import `{ postgresClient }` from './postgres-client' (matches the sibling files' actual convention — campaign-classifier.ts, remediation-service.ts, phishing-audit.ts — and the PATTERNS.md code snippet), not the plan action text's parenthetical 'default import' gloss, since the vi.mock factory only provides a named `postgresClient` export and a default-import call site would not be mocked."
  - "mergeTimeline's report/classification/audit source shapes use camelCase `createdAt` fields (not snake_case `created_at`) since the read_first note specifies these consume the already-transformed camelCase API response shapes from app/api/phishing/campaigns/[id]/route.ts."

patterns-established:
  - "Pattern: exhaustive switch with `default: return {}` for extensible action-type-keyed pure derivations (mirrors mapVerdictToActions)"
  - "Pattern: discriminated union + fixed KIND_PRIORITY map for stable multi-source chronological merges"

requirements-completed: [REVIEW-01, REVIEW-02, REVIEW-04]

# Metrics
duration: 12min
completed: 2026-07-16
---

# Phase 22 Plan 01: Wave 0 Pure-Logic Extraction Summary

**Three pure `lib/services/*.ts` modules (ticket->campaign resolver, 7-action-type default-param derivation, 3-source timeline merge) extracted and unit-tested under vitest, unblocking the route/component work in plans 02/04/05.**

## Performance

- **Duration:** 12 min
- **Started:** 2026-07-16T18:17:00Z
- **Completed:** 2026-07-16T18:29:00Z
- **Tasks:** 3 completed
- **Files modified:** 6 (all new)

## Accomplishments
- `resolveTicketToCampaign(ticketId)` covers all three resolution states (no report / ungrouped / grouped) via a single parameterized `reports` lookup
- `deriveDefaultParams(actionType, evidence)` implements the exact 7-action-type default-params table from the UI-SPEC, with a safe `{}` fallback for unknown/future action types
- `mergeTimeline(reports, classifications, auditEvents)` produces a single ascending-chronological array with a discriminated `TimelineEntry` union and a deterministic tie-break order
- All 3 modules are pure or DB-only (no `NextResponse`/`requirePermission` in any of them) — route-free per the plan's design intent

## Task Commits

Each task was committed via full TDD RED/GREEN pairs:

1. **Task 1: phishing-ticket-resolver.ts + test** — `20b1e1b` (test), `e619321` (feat)
2. **Task 2: remediation-default-params.ts + test** — `c64a905` (test), `86cffc4` (feat)
3. **Task 3: phishing-timeline.ts + test (server-merge)** — `37f6657` (test), `d30a49f` (feat)

**Plan metadata:** (this commit)

## Files Created/Modified
- `lib/services/phishing-ticket-resolver.ts` - `resolveTicketToCampaign(ticketId)`, parameterized `reports` lookup
- `lib/services/phishing-ticket-resolver.test.ts` - 4 tests covering the 3 resolution states + query-shape assertion
- `lib/services/remediation-default-params.ts` - `deriveDefaultParams(actionType, evidence)`, exhaustive switch over 7 action types + fallback
- `lib/services/remediation-default-params.test.ts` - 9 tests covering all 7 known types, unknown fallback, and null-evidence defaults
- `lib/services/phishing-timeline.ts` - `mergeTimeline(reports, classifications, auditEvents)`, `TimelineEntry` discriminated union
- `lib/services/phishing-timeline.test.ts` - 4 tests covering sort order, length invariant, per-variant fields, and tie-break stability

## Decisions Made
- Named import for `postgresClient` (not the plan text's literal "default import" parenthetical) — see `key-decisions` above; verified against all 3 sibling files (`campaign-classifier.ts`, `remediation-service.ts`, `phishing-audit.ts`) and the working `vi.mock` test pattern.
- `mergeTimeline` inputs are camelCase (`createdAt`, `reportId`, `ticketNumber`, `companyName`, `verdict`, `confidence`, `eventType`, `actor`, `payload`) since this function consumes the campaign-detail route's already-transformed response shape, not raw DB rows.

## Deviations from Plan

None - plan executed exactly as written, with one clarifying correction to an internal inconsistency in the plan text (see Decisions Made: named vs. default import) resolved in favor of the working, testable convention already established in this phase's sibling files.

## Issues Encountered
None - all three RED phases confirmed failing before implementation (module-not-found), all three GREEN phases passed on first implementation attempt.

## TDD Gate Compliance

All three tasks followed the full RED -> GREEN cycle with separate commits:
- Task 1: `test(22-01)` at `20b1e1b` -> `feat(22-01)` at `e619321`
- Task 2: `test(22-01)` at `c64a905` -> `feat(22-01)` at `86cffc4`
- Task 3: `test(22-01)` at `37f6657` -> `feat(22-01)` at `d30a49f`

No REFACTOR commits were needed — each GREEN implementation passed cleanly without follow-up cleanup.

## User Setup Required

None - no external service configuration required.

## Next Phase Readiness

- Plan 02 (ticket->campaign resolver route) can now call `resolveTicketToCampaign` directly — the route becomes a thin `requirePermission` + param-parse + call wrapper per PATTERNS.md.
- Plans 04/05 (Action Area Card, Timeline Card) have a tested `deriveDefaultParams`/`mergeTimeline` to call from their respective routes/components (or the extended `campaigns/[id]` route, per the server-merge decision).
- Full test suite: 411/413 passing. The 2 failures are pre-existing, unrelated `lib/services/analyzer/itglue-search.test.ts` failures (untouched by this plan) — logged in `.planning/phases/22-approval-ui-livelink-addressable-campaign-review-and-approve/deferred-items.md`, out of scope per the plan's file list.
- `npx tsc --noEmit --pretty` is clean.

---
*Phase: 22-approval-ui-livelink-addressable-campaign-review-and-approve*
*Completed: 2026-07-16*

## Self-Check: PASSED

All 6 created files verified present on disk; all 6 task commit hashes
(`20b1e1b`, `e619321`, `c64a905`, `86cffc4`, `37f6657`, `d30a49f`) verified
present in `git log --oneline --all`.
