---
phase: 23-classification-disposition-per-client-automation-gate
plan: 05
subsystem: phishing-triage-automation
tags: [webhook, phishing, postgres, vitest, autotask, gate-pattern]

# Dependency graph
requires:
  - phase: 23-01
    provides: "USER_AWARENESS verdict on Verdict union, classifyCampaign, generateAndPostAcknowledgment (acknowledge_user note writer)"
  - phase: 23-03
    provides: "phishing_automation_gate table (migration 100) + admin CRUD routes for per-company auto_parse/auto_classify/auto_report toggles"
provides:
  - "getCompanyAutomationGate(companyId) reusable COALESCE-false reader"
  - "Automatic parse -> classify -> acknowledge_user pipeline wired into the live Autotask webhook, gated per-company"
affects: [23-remaining-plans, phishing-triage-runbook]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Per-company opt-in settings reader (COALESCE(..., false), absent row = all-false) as a standalone testable module, mirroring the admin route's LEFT JOIN pattern but scoped to a single company_id"
    - "Gated webhook stage chain: each automation stage wrapped in its own try/catch so a stage failure logs-and-continues without blocking the fire-and-forget webhook response or aborting later stages"

key-files:
  created:
    - lib/services/phishing-automation-gate.ts
    - lib/services/phishing-automation-gate.test.ts
  modified:
    - lib/services/webhook-service.ts

key-decisions:
  - "auto_report's automatic behavior is narrowly scoped to generateAndPostAcknowledgment for USER_AWARENESS verdicts only — no other action/verdict is ever auto-posted or auto-approved (D-04 carve-out, T-23-08)"
  - "When auto_report is enabled but auto_classify did not run in the same pass (or failed), the current verdict is read from the most recent classifications row for the campaign rather than re-running classification"
  - "Detection + grouping remain unconditional for every company regardless of gate state (D-07) — only the three post-grouping stages are gated"

patterns-established:
  - "runGatedPhishingStages(input) as the single gated-stage entry point, called only when groupReportIntoCampaign resolves a campaignId"

requirements-completed: [AUTOGATE-03]

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

# Phase 23 Plan 05: Wire Gated Automation Pipeline Into Webhook Summary

**Adds a tested `getCompanyAutomationGate` reader and wires the opted-in parse -> classify -> acknowledge_user chain into the live Autotask webhook, with detection/grouping staying always-on and every other remediation action staying manual-approval-gated.**

## Performance

- **Duration:** ~15 min
- **Started:** 2026-07-16T23:31:00Z (approx)
- **Completed:** 2026-07-16T23:46:41Z
- **Tasks:** 2/2 completed
- **Files modified:** 3 (2 created, 1 modified)

## Accomplishments
- `getCompanyAutomationGate(companyId)` reusable reader: absent row / null / NaN companyId all resolve to all-false without throwing; present row maps snake_case -> camelCase.
- `triggerPhishingDetection` now captures `groupReportIntoCampaign`'s result and, when a campaign exists, runs the new `runGatedPhishingStages` method — automatically parsing, classifying, and (only for USER_AWARENESS) posting the acknowledge_user note for opted-in companies, entirely within the existing fire-and-forget call site.
- Detection and grouping remain unconditional (D-07); no other verdict/action (block_sender, purge_message, warn_user, reset_password, etc.) is ever auto-posted or auto-approved by this change.

## Task Commits

Each task was committed atomically:

1. **Task 1: Create getCompanyAutomationGate reader + tests** - `0d7974c` (test, RED) -> `e0f22f2` (feat, GREEN)
2. **Task 2: Wire gated parse->classify->acknowledge chain into triggerPhishingDetection** - `e1193bf` (feat)

_TDD gate sequence for Task 1 confirmed in git log: test commit `0d7974c` precedes feat commit `e0f22f2`._

## Files Created/Modified
- `lib/services/phishing-automation-gate.ts` - `getCompanyAutomationGate(companyId)`: COALESCE(..., false)-backed per-company gate reader, never throws.
- `lib/services/phishing-automation-gate.test.ts` - 4 vitest cases: absent row, present row mapping, null companyId, NaN companyId.
- `lib/services/webhook-service.ts` - `triggerPhishingDetection` now captures the grouping result and calls new private `runGatedPhishingStages`, which reads the gate and conditionally runs `parseAndStoreMessage`, `classifyCampaign`, and `generateAndPostAcknowledgment` (guarded by `verdict === 'USER_AWARENESS'`), each in its own try/catch.

## Decisions Made
- Verdict-for-report lookup: when `auto_classify` didn't run in the same pass (either disabled or its try/catch caught an error), `auto_report`'s USER_AWARENESS check falls back to `SELECT verdict FROM classifications WHERE campaign_id = $1 ORDER BY created_at DESC LIMIT 1` rather than skipping the report stage entirely — this lets a company with only `auto_report` enabled (classification done via the existing manual route) still get the automatic acknowledge_user post once a human/prior automated pass has classified the campaign as USER_AWARENESS.
- Kept `runGatedPhishingStages` as a new private method (not inlined into `triggerPhishingDetection`) to keep the always-on detect+group logic visually and structurally separate from the gated stages, per the plan's D-07 emphasis.

## Deviations from Plan

None - plan executed exactly as written. Both tasks matched the plan's `<action>` and interface contracts (`ParseAndStoreResult`, `ClassifyResult.verdict`, `TriageNoteResult`, `GroupReportResult`) exactly as they already existed on disk from plans 23-01 and 23-03.

## Known Stubs

None.

## Threat Flags

None — this plan's threat model (T-23-08 through T-23-11) was fully addressed as designed; no new unmodeled surface was introduced. `runGatedPhishingStages` adds no new inbound endpoint, only conditional internal calls to existing, already-reviewed service functions.

## Verification

- `npx vitest run lib/services/phishing-automation-gate.test.ts` — 4/4 passed.
- `npx tsc --noEmit --pretty` — clean, no errors.
- Full `npx vitest run` — 434 passed, 2 failed. The 2 failures are pre-existing and unrelated (`lib/services/analyzer/itglue-search.test.ts`, files never touched by this plan) — logged to `.planning/phases/23-classification-disposition-per-client-automation-gate/deferred-items.md` per the scope-boundary rule rather than fixed here.
- Acceptance-criteria greps for Task 2 all pass: `getCompanyAutomationGate` called with `companyId` (the ticket's `company_id`), `USER_AWARENESS` guards the `generateAndPostAcknowledgment` call, no auto-call to approve/remediate/block/purge/warn_user services was added, `detectPhishingTicket`/`groupReportIntoCampaign` remain unconditional, and each of the three gated stages has its own try/catch.

## Self-Check: PASSED

- FOUND: lib/services/phishing-automation-gate.ts
- FOUND: lib/services/phishing-automation-gate.test.ts
- FOUND: lib/services/webhook-service.ts (modified)
- FOUND commit 0d7974c
- FOUND commit e0f22f2
- FOUND commit e1193bf
