# Plan: Outlook sync Graph-404 fix (reverse-reconstructed from /investigate)

This plan was reconstructed from the Round 1 + Round 2 /investigate findings that drove commit `55574f65 fix(outlook-sync): Graph URL + dual-column delta-link consistency`. There was no formal /blueprint — the work was investigation-driven. /verify is auditing the implementation against the investigation deliverables.

**Scope:** `lib/integrations/providers/microsoft/outlook.ts`, `lib/integrations/sync/emailSync.ts`, `lib/integrations/tokenManager.ts`, `app/api/integrations/email/sync/route.ts`, `app/api/cron/sync-emails/route.ts`, `app/api/outlook-sync/subscribe/route.ts`, `app/settings/integrations/IntegrationsClient.tsx`, `lib/integrations/providers/microsoft/__tests__/outlook.test.ts`, Vercel env vars.

---

## Backend

### Item 1 — Fix `/me/inbox/messages/delta` URL in `getOutlookMessagesDelta`
- **Acceptance:** URL constructed is `/me/mailFolders/{folder}/messages/delta` (Microsoft Graph documented form), never `/me/{folder}/messages/delta` shortcut.
- **Expected:** `lib/integrations/providers/microsoft/outlook.ts` — folder branch around line 455-457 of the original.
- **Verifiability:** concrete — grep for `mailFolders/` in URL construction; assert no `/me/${folder}/` pattern remains.

### Item 2 — Fix `/me/inbox/messages` URL in `listOutlookMessages`
- **Acceptance:** Same URL pattern — `/me/mailFolders/{folder}/messages`.
- **Expected:** `outlook.ts` line ~334.
- **Verifiability:** concrete.

### Item 3 — Stale-deltaLink recovery
- **Acceptance:** When a `deltaLink` arg is provided AND its URL matches `/me/<non-mailFolders-segment>/messages/delta` AND Graph returns 404 + `ResourceNotFound`, throw `DeltaTokenExpired` so callers fall back to a fresh initial sync.
- **Expected:** `outlook.ts` after Microsoft-error parse, before the generic throw. Restricted to the `deltaLink` branch so it can't mask fresh-URL bugs.
- **Verifiability:** concrete — read code + new test asserting this path.

### Item 4 — `OutlookMailboxNotProvisioned` typed error
- **Acceptance:** New exported `OutlookMailboxNotProvisioned extends Error` class. Thrown on 404 + code in {`MailboxNotEnabledForRESTAPI`, `ErrorMailboxNotEnabledForRESTAPI`}. NOT thrown on `MailboxItemNotFound` (per-item, not per-mailbox).
- **Expected:** `outlook.ts` — exported class + throw site.
- **Verifiability:** concrete — class export + throw site + discrimination test.

### Item 5 — Diagnostic logging on Graph errors
- **Acceptance:** On any Graph 4xx/5xx (except 410, which is already handled as a known DeltaTokenExpired path), log: status, code, URL path (without querystring), error message (truncated), and raw body preview (truncated). No PII/tokens.
- **Expected:** `outlook.ts:getOutlookMessagesDelta` error branch.
- **Verifiability:** concrete — read code + verify log shape.

### Item 6 — Catch `OutlookMailboxNotProvisioned` in `syncOutlookEmails`, deactivate token
- **Acceptance:** Typed `instanceof OutlookMailboxNotProvisioned` catch. Sets `is_active=false`, writes human-readable `last_error`, returns `{ success: false, error: humanError }`. Uses admin client; filters by `tokenId` AND `userId`.
- **Expected:** `emailSync.ts:syncOutlookEmails` catch block.
- **Verifiability:** concrete.

### Item 7 — Dual-column delta-link read in manual sync route
- **Acceptance:** For `provider='azure'`, route reads `outlook_message_delta_link_encrypted` first, falls back to `email_sync_token`. Matches webhook reader.
- **Expected:** `app/api/integrations/email/sync/route.ts` azure branch.
- **Verifiability:** concrete.

### Item 8 — Dual-column delta-link read in cron sync route
- **Acceptance:** Select clause includes `outlook_message_delta_link_encrypted`. For azure provider, syncToken prefers new col.
- **Expected:** `app/api/cron/sync-emails/route.ts`.
- **Verifiability:** concrete.

### Item 9 — Dual-column delta-link write in `emailSync.updateEmailSyncToken`
- **Acceptance:** Function takes `provider: 'google' | 'azure'` param. For `'azure'`, writes BOTH `email_sync_token` AND `outlook_message_delta_link_encrypted`. For `'google'`, writes only `email_sync_token`.
- **Expected:** `lib/integrations/sync/emailSync.ts`.
- **Verifiability:** concrete.

### Item 10 — Dual-column delta-link write in `tokenManager.updateEmailSyncToken`
- **Acceptance:** Orphan export function in tokenManager.ts also provider-aware. (Currently unused but exported — must not be a footgun.)
- **Expected:** `lib/integrations/tokenManager.ts`.
- **Verifiability:** concrete.

### Item 11 — Subscribe-route seed-failure preservation
- **Acceptance:** When `getOutlookMessagesDelta` seed call fails (returns no `@odata.deltaLink`), the `user_provider_tokens` UPDATE must NOT include the delta-link columns (preserves any prior valid cursor). When seed succeeds, both columns get the new value.
- **Expected:** `app/api/outlook-sync/subscribe/route.ts`.
- **Verifiability:** concrete.

### Item 12 — `StoredProviderToken` type + select includes `outlook_message_delta_link_encrypted`
- **Acceptance:** Field on interface; column in `getStoredTokenRecord` select.
- **Expected:** `lib/integrations/tokenManager.ts`.
- **Verifiability:** concrete.

### Item 13 — Email sync route aggregates per-provider errors
- **Acceptance:** Response body includes top-level `error` field aggregating errors from failed providers (e.g., `"azure: <message>; google: <message>"`).
- **Expected:** `app/api/integrations/email/sync/route.ts`.
- **Verifiability:** concrete.

## Frontend

### Item 14 — IntegrationsClient surfaces `body.error` on HTTP 200 with `success: false`
- **Acceptance:** After HTTP 200, if `body.success === false && body.error`, `setError(body.error)`.
- **Expected:** `app/settings/integrations/IntegrationsClient.tsx` `handleSync`.
- **Verifiability:** concrete.

## Tests

### Item 15 — URL form asserts in tests
- **Acceptance:** Tests for `listOutlookMessages` and `getOutlookMessagesDelta` assert `/me/mailFolders/` in URL, reject `/me/inbox/`.
- **Expected:** `lib/integrations/providers/microsoft/__tests__/outlook.test.ts`.
- **Verifiability:** concrete.

### Item 16 — Stale-shortcut recovery test
- **Acceptance:** Test asserting old stored `/me/inbox/...` deltaLink → `DeltaTokenExpired` on `ResourceNotFound` 404.
- **Expected:** outlook.test.ts.
- **Verifiability:** concrete.

### Item 17 — `OutlookMailboxNotProvisioned` discrimination test
- **Acceptance:** Tests for 404 + `MailboxNotEnabledForRESTAPI` → typed error; 404 + `MailboxItemNotFound` → NOT typed error (falls through to generic).
- **Expected:** outlook.test.ts.
- **Verifiability:** concrete.

## Config

### Item 18 — Vercel env vars cleaned up
- **Acceptance:**
  - `AZURE_TENANT_ID='common'` in all 3 environments (production, preview, development), `--no-sensitive`.
  - `NEXT_PUBLIC_SUPABASE_URL='https://auth.laureo.io'` in all 3 envs, `--no-sensitive`, no trailing `\n` (byte-verified).
  - `SUPABASE_URL='https://auth.laureo.io'` (server-only sibling) — same.
- **Verifiability:** concrete via `vercel env pull` + `od -c`.

---

## Plan-quality fields required by CLAUDE.md (audited in Phase 1)

- All Supabase writes filter by `organization_id` where applicable.
- Manual sync route uses `withAuth` (mutations).
- Cron route uses `verifyCronRequest` auth.
- Subscribe route uses `withAuth`.
- All routes return `{ error: string }` on failure with appropriate status codes.
- Tests added for new behavior.
- No new secrets committed.
- Migrations are additive (none required for this fix — schema columns already exist from prior parity migrations).
