---
phase: 13-scheduler-admin-toggle
plan: 03
type: execute
wave: 2
depends_on: [13-01, 13-02]
files_modified: []
autonomous: false
requirements: [PAX8-07, PAX8-09]
must_haves:
  truths:
    - "The pax8-daily schedule can be enabled from the /admin schedule editor and, once enabled, fires the full PAX8 sync (companies + subscriptions + products + orders + matching) with no code deploy"
    - "PAX8 appears as a toggleable row on /admin/integrations"
    - "Disabling PAX8 from /admin/integrations stops the next scheduled pax8-daily run (skip log) AND makes POST /api/pax8/sync return 403"
    - "Re-enabling PAX8 resumes scheduled sync at the next tick with no container restart"
  artifacts: []
  key_links:
    - from: "/admin/integrations toggle"
      to: "pax8-daily scheduled run + POST /api/pax8/sync"
      via: "integration_settings.disabled gate"
      pattern: "integration_settings"
---

<objective>
Prove Phase 13's four success criteria against the running app — the behaviors that
cannot be unit-tested (cron firing, admin row rendering, live 403 gating, re-enable
resumption). This plan verifies; it does not change code.

Purpose: Confirm PAX8-07 (daily scheduled sync) and PAX8-09 (toggle on/off without
restart, with real enforcement) end-to-end before the phase closes.
Output: A human-confirmed pass of ROADMAP Phase 13 SC#1-4.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/phases/13-scheduler-admin-toggle/13-CONTEXT.md
@.planning/phases/13-scheduler-admin-toggle/13-01-PLAN.md
@.planning/phases/13-scheduler-admin-toggle/13-02-PLAN.md
</context>

<tasks>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 1: Verify Phase 13 SC#1-4 against the running app</name>
  <action>
    Do NOT change code in this plan. Ensure plans 13-01 and 13-02 are merged and the dev
    app + pulse-postgres are running with PAX8 credentials set, then walk the human through
    the verification steps below and collect a pass/fail per success criterion.
  </action>
  <what-built>
    - Migration 096 seeded a `pax8-daily` row in sync_schedules (cron 0 4 * * *, is_enabled=false).
    - `executeScheduledSync` has a dual-guarded pax8-daily branch calling `getPax8SyncService().fullSync('scheduled')` — skips when PAX8 is not configured or DB-disabled.
    - `checkIntegrationHealth` returns a `pax8` row, so PAX8 renders on /admin/integrations.
    - `POST /api/pax8/sync` returns 403 when PAX8 is disabled.
  </what-built>
  <how-to-verify>
    Preconditions: PAX8_CLIENT_ID and PAX8_CLIENT_SECRET are set in the environment, and the dev app + pulse-postgres are running (docker compose up). Sign in to the app first (the /api/pax8/sync route requires a session — it is NOT in middleware's public list).

    SC#2 — PAX8 is a toggleable row:
    1. Open `/admin/integrations`. Confirm a "PAX8" row appears (category Finance) with an enable/disable control, alongside the other integrations.

    SC#1 — pax8-daily is scheduled and fires:
    2. Confirm the schedule row exists:
       `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT id, cron_expression, sync_type, is_enabled FROM sync_schedules WHERE sync_type='pax8-daily'"`
       Expect: `pax8-daily|0 4 * * *|pax8-daily|f`.
    3. Enable the pax8-daily schedule from the /admin schedule editor (or temporarily set its cron to a near-future minute for the test). Confirm a scheduled tick logs `[SCHEDULER]` running pax8-daily and that a PAX8 sync actually runs (watch app logs and/or `SELECT * FROM sync_history WHERE entity_type='pax8' ORDER BY started_at DESC LIMIT 3`). Confirm it ran the full sequence (companies, subscriptions, products, orders, matching) — i.e. `getPax8SyncService().fullSync('scheduled')`, not a partial. Restore the real cron `0 4 * * *` afterward.

    SC#3 — disabling stops runs AND blocks the manual route:
    4. On `/admin/integrations`, DISABLE PAX8 (optionally with a disabled_reason). Confirm the toggle records disabled_by / disabled_at (visible in the UI or `SELECT key, disabled, disabled_by, disabled_at, disabled_reason FROM integration_settings WHERE key='pax8'`).
    5. Trigger the manual route while disabled (from an authenticated browser session, e.g. devtools fetch, or curl with your session cookie):
       `curl -i -X POST http://localhost:3100/api/pax8/sync -b "<your-session-cookie>"`
       Expect HTTP 403 with body `{"error":"PAX8 is disabled", ...}`. (Without a session you will get redirected/401 — that is auth, not the gate.)
    6. Wait for (or force) the next scheduled tick while disabled. Confirm the app logs `Skipping pax8-daily — PAX8 disabled via /admin/integrations` and that NO new pax8 sync_history row was created.

    SC#4 — re-enable resumes with no restart:
    7. RE-ENABLE PAX8 on /admin/integrations (no container restart). Confirm `POST /api/pax8/sync` now returns 200 (`{"ok":true,...}`) and the next scheduled tick runs the sync again.
  </how-to-verify>
  <resume-signal>Type "approved" if all four success criteria pass, or describe which step failed (include the observed log line / HTTP status / SQL result).</resume-signal>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| /admin/integrations toggle → sync execution | The disable toggle must govern both the cron dispatch and the manual route — verified live here |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-13-01 | Elevation of Privilege | disable-toggle enforcement | mitigate | Step 5 (manual 403) and Step 6 (cron skip log, no sync_history row) verify the disabled toggle actually stops both paths — the security gate's core requirement |
| T-13-SC | Tampering | npm/pip/cargo installs | accept | No package installs — verification only |
</threat_model>

<verification>
Human confirms ROADMAP Phase 13 SC#1 (scheduled + fires full sync), SC#2 (toggleable row),
SC#3 (disable stops cron + returns 403), SC#4 (re-enable resumes, no restart).
</verification>

<success_criteria>
- All four Phase 13 success criteria pass against the running app.
- The disable toggle demonstrably stops both the scheduled run and the manual route (no side door).
</success_criteria>

<output>
Create `.planning/phases/13-scheduler-admin-toggle/13-03-SUMMARY.md` when done.
</output>
