--- Task ID: refactor-1 Agent: general-purpose Task: Fix all TypeScript compilation errors Work Log: - src/app/api/aos/webhooks/route.ts: Extracted the PATCH handler (which expected a `{ id }` param but lived at the top-level route) into a new dynamic route file `src/app/api/aos/webhooks/[id]/route.ts`. Removed PATCH from the top-level `route.ts`. This also resolved the cascading `.next/dev/types/validator.ts` RouteHandlerConfig error. - src/app/api/aos/kb/route.ts: Replaced the broken inline `where` type annotation with `Prisma.ArticleWhereInput` (imported `Prisma` from `@prisma/client`). The previous type declared `OR` as a single object, but the code assigns an array; the Prisma-generated type is the source of truth and accepts the `OR` array of filter objects including `excerpt`. - src/app/api/aos/setup/test-mail/route.ts (line 57): Changed `banner: banner || null` to `banner: banner || undefined` so the value matches the `banner?: string` property on `MailTestResult` (optional string, not `string | null`). - src/components/aos/mobile-bottom-nav.tsx: Widened the `items` array type from `{ id: ViewId | 'search' | 'menu'; ... }` to `{ id: string; ... }` so `'notifications'` is allowed. Updated `handleClick` to take `id: string` (still casts to `ViewId` for the real view setter). Updated the `active` computation to cast the empty-string sentinel (`'' as ViewId`) so the equality check type-checks; behavior is unchanged (search/menu are never "active", notifications maps to 'calendar'). - src/components/aos/views/crm.tsx (line 268): Changed `p.companyId && onSelectCompany(p.companyId)` to `p.company?.id && onSelectCompany(p.company.id)` since the `Person` type only exposes the `company` relation, not a flat `companyId`. - src/components/aos/views/estimates.tsx: Added `projectId?: string | null` to the `Estimate` interface (line ~50) so the chain-indicator logic that reads `selected.projectId` type-checks. - src/components/aos/views/support.tsx: Added `createdAt: string` to the `Ticket` interface (line ~38) so the `t.createdAt` fallback in `formatRelative(...)` on line 220 type-checks. Stage Summary: - All 11 TypeScript errors previously reported under `src/` are resolved. `npx tsc --noEmit 2>&1 | grep "src/"` now returns no `src/` errors (the only remaining error is in `skills/stock-analysis-skill/src/analyzer.ts`, explicitly out of scope per task instructions). - Bonus: fixing the webhooks route signature also cleared the related `.next/dev/types/validator.ts` RouteHandlerConfig error. - Next actions: none required from this task. Optionally, run the dev server / build once to confirm runtime behavior, and consider removing the now-empty trailing blank lines in `src/app/api/aos/webhooks/route.ts`. --- Task ID: refactor-2 Agent: general-purpose Task: Add try/catch error handling to API route handlers performing database writes (POST/PATCH/DELETE) and to complex GET aggregation handlers that were missing it. Work Log: - Wrapped every previously-uncaught handler body in `try { … } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error'; return NextResponse.json({ error: msg }, { status: 500 }) }`, re-indenting the body one level so it sits inside the `try` block. - Write handlers updated (14 files, 15 handlers): 1. src/app/api/aos/crm/deals/route.ts — POST 2. src/app/api/aos/estimates/[id]/convert/route.ts — POST 3. src/app/api/aos/accounting/bank/[id]/match/route.ts — POST 4. src/app/api/aos/accounting/vat/[id]/submit/route.ts — PATCH 5. src/app/api/aos/expenses/[id]/convert/route.ts — POST 6. src/app/api/aos/api-tokens/[id]/revoke/route.ts — PATCH 7. src/app/api/aos/api-tokens/route.ts — POST (GET left untouched — simple list) 8. src/app/api/aos/webhooks/route.ts — POST (GET left untouched — simple list) 9. src/app/api/aos/webhooks/[id]/route.ts — PATCH 10. src/app/api/aos/projects/[id]/tasks/route.ts — POST 11. src/app/api/aos/tasks/[id]/comments/route.ts — POST (GET left untouched — simple list) 12. src/app/api/aos/leads/convert/route.ts — POST 13. src/app/api/aos/settings/route.ts — PATCH (GET left untouched — simple list) 14. src/app/api/aos/leads/[id]/route.ts — PATCH - Complex aggregation GET handlers updated (6 files): 15. src/app/api/aos/dashboard/route.ts — GET (9-table Promise.all + KPI math) 16. src/app/api/aos/notifications/route.ts — GET (9-table Promise.all + per-row derivation) 17. src/app/api/aos/calendar/route.ts — GET (8-table aggregation + grid builder) 18. src/app/api/aos/reporting/route.ts — GET (10-table Promise.all + 4 aggregation sections) 19. src/app/api/aos/accounting/route.ts — GET (5-table Promise.all + EÜR/bank/dunning) 20. src/app/api/aos/timesheet/route.ts — GET (week grid builder) AND PATCH (week-lock write) - Did NOT touch handlers that already had try/catch (AI endpoints, seed endpoint, test-mail, MCP, etc.) as instructed. - Did NOT touch simple GET list handlers (api-tokens GET, webhooks GET, tasks comments GET, settings GET) since they are safe single-table reads — left as-is per task spec ("For GET handlers, they're generally safe…"). Stage Summary: - `npx tsc --noEmit 2>&1 | grep "^src/"` returns empty — no `src/` TypeScript errors introduced. (Pre-existing errors remain only in `examples/websocket/*.tsx` and `skills/*`, both explicitly out of scope per the previous task's worklog.) - `bun run lint` exits with code 0 — no ESLint errors or warnings. - All 21 handlers (14 write + 6 complex-GET + 1 timesheet GET) now return a 500 JSON response `{ error: }` instead of crashing the request when Prisma throws, JSON parsing fails, or any other runtime error occurs inside the handler. The `err instanceof Error ? err.message : 'internal server error'` guard mirrors the requested pattern verbatim and prevents leaking raw stack traces while still returning useful error text for legitimate failures (e.g. unique-constraint violations on token/webhook creation). - Next actions: none required. Optionally, consider refactoring the duplicated catch block into a shared helper (e.g. `withErrorHandling()` higher-order function) in a follow-up task to reduce boilerplate across the 21 handlers. --- Task ID: refactor-3 Agent: general-purpose Task: Add try/catch error handling to the remaining GET-only API routes that were still missing it (read-only handlers but should still gracefully handle DB errors), plus fix the silent `.catch(() => {})` in `src/lib/api-auth.ts`. Work Log: - Wrapped every previously-uncaught GET handler body in `try { … } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error'; return .json({ error: msg }, { status: 500 }) }`, re-indenting the body one level so it sits inside the `try` block. Used `Response.json` in the `/api/v1/*` routes (which already use the global `Response`) and `NextResponse.json` in the `/api/aos/*` routes (which already import `NextResponse`) to keep each file consistent with its existing return style. - GET handlers updated (17 files, 17 handlers): 1. src/app/api/v1/companies/route.ts — GET (public API: companies list with auth + scope check) 2. src/app/api/v1/invoices/route.ts — GET (public API: invoices list) 3. src/app/api/v1/licenses/route.ts — GET (public API: licenses list) 4. src/app/api/aos/portal/companies/[id]/notifications/route.ts — GET (5-table Promise.all + per-row notif derivation) 5. src/app/api/aos/portal/companies/[id]/route.ts — GET (company dashboard: multi-include + tickets + approvals + KPIs) 6. src/app/api/aos/portal/companies/route.ts — GET (portal-eligible companies list) 7. src/app/api/aos/crm/companies/[id]/route.ts — GET (company detail + audit + timeline + consent) 8. src/app/api/aos/crm/route.ts — GET (3-table Promise.all + pipeline grouping) 9. src/app/api/aos/estimates/route.ts — GET (estimates + secondary company lookup + stats) 10. src/app/api/aos/kb/route.ts — GET (KB articles + filter + stats) 11. src/app/api/aos/finance/route.ts — GET (invoices + e-Rechnung stats) 12. src/app/api/aos/expenses/route.ts — GET (expenses + company/project enrichment + byCategory + 6-month report) 13. src/app/api/aos/tickets/route.ts — GET (ticket inbox + SLA derivation + stats) 14. src/app/api/aos/contracts/route.ts — GET (contracts + company enrichment + expiry stats) 15. src/app/api/aos/search/route.ts — GET (7-table global command-palette search) 16. src/app/api/aos/leads/route.ts — GET (leads review queue + JSON.parse of evidence column) 17. src/app/api/aos/licenses/route.ts — GET (licenses + assets Promise.all + renewal stats) - Did NOT touch `src/app/api/v1/openapi/route.ts` (static, no DB) or `src/app/api/route.ts` (static) as instructed. - src/lib/api-auth.ts line 38: replaced the silent `.catch(() => {})` on the fire-and-forget `db.apiToken.update({ … lastUsedAt … })` call with `.catch((err) => console.warn('Failed to update lastUsedAt:', err))`. This keeps the call non-blocking (the auth handler does not await it) but now logs failures instead of swallowing them, so a broken `lastUsedAt` write becomes visible in server logs rather than disappearing silently. Stage Summary: - `npx tsc --noEmit 2>&1 | grep "^src/"` returns empty — no `src/` TypeScript errors introduced. (Pre-existing errors remain only in `examples/websocket/*.tsx` and `skills/*`, both explicitly out of scope per the refactor-1 worklog.) - `bun run lint` exits with code 0 and produces no warnings/errors — clean. - All 17 GET handlers now return a 500 JSON response `{ error: }` instead of crashing the request when Prisma throws, when the URL constructor fails, or when `JSON.parse(l.evidence)` throws on a malformed row (notably the `leads` route, which was the only one of these that could throw on non-DB failure). - Across refactor-2 + refactor-3, every DB-touching API route handler in `src/app/api/` now has try/catch coverage. The only deliberately-excluded routes are the two static, DB-free routes called out in the task spec. - Next actions: none required. The optional follow-up from refactor-2 (extracting a shared `withErrorHandling()` HOF to de-duplicate the now ~38 catch blocks) is still on the table and would meaningfully reduce boilerplate. --- Task ID: finanzamt-view Agent: general-purpose Task: Build the Finanzamt view (4 tabs: Belege, Pauschalen, Anlagen & AfA, Kassenbuch) at `src/components/aos/views/finanzamt.tsx`. Work Log: - Created `src/components/aos/views/finanzamt.tsx` (~1250 lines). The view is named `FinanzamtView` and also exported as default. Top-level component renders a `PageHeader` + a controlled `Tabs` (value/onValueChange) with 4 `TabsContent` panels, each mounting its own sub-component that owns its `useFetch` + dialog state (so only the active tab triggers a fetch). - **Tab 1 — Belege (`BelegeTab` + `BelegDialog`):** - `useFetch('/api/aos/belege')` consumes `{ belege, stats }`. - 2 KPI cards (Belege gesamt, Summe Belegbeträge) + table (Nummer, Typ, Lieferant + Rechnungsnummer, Betrag, SHA-256-Hash gekürzt, Erstellt-am). - "Beleg registrieren" dialog: optional file picker (read via `FileReader.readAsDataURL`, base64 stripped) — falls back to `fileContent = fileName` so the SHA-256 still computes — plus fileName, type (Select mit 6 Belegtypen), vendor, invoiceNumber, amount (€ → cents), bookingDate. - Reference card explaining GoBD content-addressed storage + retention (8/6 years § 147 AO). - **Tab 2 — Pauschalen (`PauschalenTab` + `PauschaleDialog`):** - `useFetch('/api/aos/pauschalen')` consumes `{ pauschalen, stats, rates }`. - 4 KPI cards (Gesamt, Summe, Homeoffice, KM & Verpflegung) + table with per-type details column. - "Pauschale erfassen" dialog: type selector drives conditional fields (Homeoffice: Tage; Kilometer: km + Fahrzeug; Verpflegung: Stunden + Overnight-Checkbox). Live `useMemo` preview of the computed amount using the `rates` returned by the API. - Reference card listing the 2026 rates (Homeoffice €6.92/d, KM €0.30/km, Verpflegung €14/€24/€28) with EStG § 4 Abs. 5 / § 9 Abs. 1 citations. - **Tab 3 — Anlagen & AfA (`AnlagenTab` + `AnlageDialog`):** - `useFetch('/api/aos/anlagen')` consumes `{ anlagen, stats }`. - 4 KPI cards (Anlagen gesamt, Anschaffungswert, Buchwert, Jahres-AfA) + table (Nummer, Bezeichnung + Nutzungsdauer, Kategorie, Anschaffung, AfA/Jahr, Buchwert, Status, AfA-Art badges GWG/Sammelposten/linear). - "Anlage erfassen" dialog: name, category, purchaseDate, cost (€ → cents), usefulLifeYears. Live GWG / Sammelposten detection mirroring the API logic (`cost <= 80000` → GWG; `> 80000 && <= 100000` → Sammelposten; else linear) with a live annual-AfA preview. - Reference card: GWG § 6 Abs. 2 EStG, Sammelposten § 6 Abs. 2a EStG, lineare AfA. Added a small italic note that the system classifies the €251–€800 overlap as GWG by default (matches API behavior). - **Tab 4 — Kassenbuch (`KassenbuchTab` + `KassenbuchDialog`):** - `useFetch('/api/aos/kassenbuch')` consumes `{ entries, stats }`. - 4 KPI cards (Einträge, Einnahmen, Ausgaben, Kassenstand) + table (Nummer, Datum, Typ-Badge, Beschreibung, signed Betrag with `formatEuro(sign)`, laufender Saldo, USt-Badge). - "Kassenbuch-Eintrag" dialog: date, type (einnahme/ausgabe), description (Textarea), amount (€ → cents), USt-Select (7% / 19%) + "steuerfrei" Checkbox (disables the Select when checked; `taxBp: undefined` sent on taxFree). - Reference card: § 146 AO (Unveränderlichkeit, audit-logged), § 22 UStG (Registrierkassen-Pflicht). - Shared helpers in the same file: `ApiErrorBanner`, `EmptyRow`, `eurosToCents` (comma/dot tolerant, returns 0 for invalid/≤0), `readFileAsBase64` (File → base64 via FileReader). - All POST operations wrapped in `try { fetch… if (!r.ok) throw new Error(res.error) … toast.success } catch (err) { toast.error(err instanceof Error ? err.message : '…') } finally { setSubmitting(false) }`. All toasts use `toast.loading` → `toast.success`/`toast.error` with `{ id: t }` for in-place updates. `refetch()` is called after every successful write. - Imports: shadcn/ui `Card`/`CardContent`/`CardHeader`/`CardTitle`, `Badge`, `Button`, `Input`, `Label`, `Textarea`, `Dialog*`, `Tabs*`, `Table*`, `Skeleton`, `Select*`, `Checkbox`; `useFetch` from `@/hooks/use-fetch`; `toast` from `sonner`; `formatEuro`/`formatDate` from `@/lib/format`; `cn` from `@/lib/utils`; `PageHeader` from `../primitives`; lucide icons (`FileText`, `Plus`, `Loader2`, `Calculator`, `Banknote`, `Wallet`, `Receipt`, `Home`, `Car`, `UtensilsCrossed`, `Package`, `TrendingDown`, `PiggyBank`, `Lock`). - All UI labels in German; no `any` types — every API payload shape declared as an explicit interface; `Pauschale['type']` used as the key type for `PAUSCHALE_TYPE_META` so the map is exhaustive. - Bonus 1-line fix in `src/app/api/aos/belege/route.ts:45`: added parens around the ternary in `setFullYear(getFullYear() + type.includes('rechnung') ? 8 : 6)` → `setFullYear(getFullYear() + (type.includes('rechnung') ? 8 : 6))`. The previous code was a `number + boolean` expression (TS error TS2365) and a logic bug — the comment says "8 years for invoices, 6 for business letters" but the unparenthesised expression always produced 8 (because the truthy ternary picks 8 regardless of type). Now matches the comment and resolves the only remaining `src/` TS error. Stage Summary: - `npx tsc --noEmit 2>&1 | grep "^src/" | wc -l` → **0** (was 1 before the belege-route fix; the finanzamt.tsx file itself introduced zero new errors). - `bun run lint` → exit code **0**, no warnings/errors. - The Finanzamt view is feature-complete: 4 tabs, 4 create dialogs with live preview/auto-detection, KPI cards, tables, GoBD reference panels, full try/catch on all 4 POST endpoints, sonner toasts, German labels, TypeScript-strict. - Next actions: none required. Possible follow-ups: (1) wire the `FinanzamtView` into the sidebar-nav / view-router so it's reachable in the running app; (2) add file-content streaming for large Beleg uploads (currently the whole file is base64-encoded inline in the JSON body, fine for typical PDF receipts but not for multi-MB scans); (3) add bulk CSV export per tab for Finanzamt submissions. --- Task ID: prd-1-zod Agent: general-purpose Task: PRD §5.2 requires "Zod on every server function input". Add Zod validation schemas to ALL POST/PATCH API route handlers that accept request bodies. Work Log: - Implemented the consistent `safeParse` pattern at the top of every write handler that parses a request body: ```ts const parsed = .safeParse(await req.json()) if (!parsed.success) { return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 }) } const body = parsed.data ``` This returns HTTP 400 with a structured `{ formErrors, fieldErrors }` payload (Zod's `flatten()`) on bad input, while successful parses yield a fully-typed `body`. All existing try/catch blocks and business-logic checks (e.g. "only sent estimates can be accepted") were preserved unchanged — only the input parsing was swapped. - Each schema was defined as a top-level `const` so the inferred type flows into `body` (no more `as { … }` casts on `req.json()`). Files modified (24 files, 26 handlers): 1. `src/app/api/aos/crm/deals/route.ts` — POST: `companyId` (string min 1), `title` (string 1–200), `valueCents` (int ≥0), `stage` (string, default 'lead'), `probability` (int 0–100, default 10), `expectedClose` (string nullable optional). Removed the two manual `if (!body.companyId)` / `if (!body.title)` 400-checks since Zod handles them now. 2. `src/app/api/aos/crm/import/route.ts` — POST: `csv` (string min 1), `mapping` (`z.record(z.string(), z.number())` — Zod v4 requires both key + value schemas). Simplified the dedup check from `!mapping?.name && mapping.name !== 0` to `mapping.name === undefined`. 3. `src/app/api/aos/leads/[id]/route.ts` — PATCH: `status` (`z.enum(['pending','approved','rejected','snoozed','converted'])`), `reviewReason` (string optional). Removed the manual `includes()` enum check. 4. `src/app/api/aos/leads/convert/route.ts` — POST: `leadId` (string min 1). 5. `src/app/api/aos/leads/explain/route.ts` — POST: `leadId` (string min 1). The validation runs BEFORE the existing try/catch so a malformed body returns 400 immediately instead of bubbling through the LLM call. 6. `src/app/api/aos/estimates/[id]/accept/route.ts` — PATCH: `acceptedBy` (string optional). Kept the `req.json().catch(() => ({}))` so an absent body still validates. 7. `src/app/api/aos/projects/[id]/tasks/route.ts` — POST: `title` (string min 1), `priority` (`z.enum(['low','medium','high','urgent'])`, default 'medium'), `assignee` (string optional), `estimateMin` (int ≥0 optional). 8. `src/app/api/aos/projects/[id]/time/route.ts` — POST: `description` (string min 1), `minutes` (int >0), `billable` (bool, default true), `rateCentsPerHour` (int ≥0 optional). 9. `src/app/api/aos/finance/[id]/finalize/route.ts` — PATCH: `zugferd` (bool optional), `xrechnung` (bool optional). Kept the `req.json().catch(() => ({}))` pattern. 10. `src/app/api/aos/contracts/[id]/sign/route.ts` — PATCH: `signedBy` (string optional). Kept the `req.json().catch(() => ({}))` pattern. 11. `src/app/api/aos/accounting/bank/[id]/match/route.ts` — POST: `invoiceId` (string min 1). 12. `src/app/api/aos/accounting/bank/import/route.ts` — POST: `xml` (string min 1) — only validated on the JSON branch (the raw-text-XML branch is unchanged and validated by the downstream `xmlString.trim().startsWith('<')` check, since the body isn't JSON in that case). 13. `src/app/api/aos/api-tokens/route.ts` — POST: `name` (string min 1), `scopes` (`z.array(z.string()).min(1)`), `expiresAt` (string optional). Removed the manual `name?.trim()` and `scopes?.length` checks; kept the secondary `invalid = scopes.filter((s) => !ALL_SCOPES.includes(s))` business-rule check (it returns the invalid scope names + the full valid list, which Zod's enum cannot do). 14. `src/app/api/aos/webhooks/route.ts` — POST: `url` (`z.string().min(1).startsWith('https://')`), `events` (`z.array(z.string()).min(1)`). Removed the three manual `url?.trim()` / `url.startsWith('https://')` / `events?.length` checks; kept the secondary `invalid = events.filter((e) => !KNOWN_EVENTS.includes(e) && e !== '*')` check (same reason as tokens — it returns the valid-event list). 15. `src/app/api/aos/timesheet/route.ts` — PATCH: `week` (`z.string().regex(/^\d{4}-\d{2}-\d{2}$/)`). Removed the manual `!week` check; kept the downstream `isNaN(refDate.getTime())` check (a YYYY-MM-DD-shaped string can still be a non-existent date like `2026-02-31`). 16. `src/app/api/aos/tasks/[id]/comments/route.ts` — POST: `body` (string min 1), `author` (string optional). Removed the manual `body?.trim()` check. 17. `src/app/api/aos/portal/approvals/[id]/decide/route.ts` — PATCH: `decision` (`z.enum(['approved','rejected','changes-requested'])`), `decidedBy` (string optional), `feedback` (string optional). Removed the manual `includes()` enum check. 18. `src/app/api/aos/kb/generate/route.ts` — POST: `topic` (string min 1), `category` (string optional), `audience` (`z.enum(['internal','client','public'])` optional). Removed the manual `topic?.trim()` check. 19. `src/app/api/aos/belege/route.ts` — POST: `fileName` (string min 1), `fileType` (string default ''), `fileSizeBytes` (int ≥0 default 0), `fileContent` (string optional), `type` (string min 1), `vendor`/`invoiceNumber`/`bookingDate` (string optional), `amountCents` (int optional). Removed the manual `fileName?.trim()` check. 20. `src/app/api/aos/pauschalen/route.ts` — POST: `type` (`z.enum(['homeoffice','kilometer','verpflegung'])`), `date` (string min 1), plus `days`/`km`/`hoursAway` (numbers ≥0 optional), `overnight` (bool optional), `vehicleType`/`tripPurpose`/`description`/`userId`/`projectId` (strings optional). Removed the manual `body.type || !body.date` check. 21. `src/app/api/aos/anlagen/route.ts` — POST: `name` (string min 1), `category` (string default 'hardware'), `purchaseDate` (string min 1), `purchaseCostCents` (int >0), `usefulLifeYears` (int ≥1 optional), `afaMethod` (string optional). Removed the two manual `!name?.trim()` / `!purchaseCostCents || ≤0` checks. 22. `src/app/api/aos/kassenbuch/route.ts` — POST: `date` (string min 1), `type` (`z.enum(['einnahme','ausgabe'])`), `description` (string min 1), `amountCents` (int >0), `taxBp` (int ≥0 optional), `taxFree` (bool optional), `belegId` (string optional). Removed the three manual `description` / `amountCents` / `type` 400-checks. 23. `src/app/api/aos/buchhaltung/route.ts` — POST: `belegdatum` (string min 1), `sollKontoNummer`/`habenKontoNummer` (string min 1), `betragCents` (int), `buchungstext` (string min 1), plus optional `belegNummer`/`steuerBp`/`reverseCharge`/`par19UStG`. PATCH: `buchungId` (string min 1). 24. `src/app/api/aos/buchhaltung/jahresabschluss/route.ts` — POST: `jahr` (`z.number().int().min(1900).max(9999)`). Removed the manual `!jahr` check. 25. `src/app/api/aos/setup/test-mail/route.ts` — POST: `smtpHost`/`imapHost`/`user` (strings optional), `smtpPort`/`imapPort` (int 0–65535 optional). Kept the `req.json().catch(() => ({}))` pattern so an empty body still validates. Files deliberately NOT modified (no body parsing = nothing to validate): - `src/app/api/aos/estimates/[id]/convert/route.ts` — POST handler takes `_req` and only uses the URL `[id]` param; no `req.json()` call. - `src/app/api/aos/expenses/[id]/convert/route.ts` — same: `_req` only, uses URL `[id]`. - `src/app/api/aos/accounting/vat/[id]/submit/route.ts` — PATCH handler takes `_req` and only uses URL `[id]`; no body parsing. - `src/app/api/aos/recurring/run/route.ts` — POST handler takes no `req` param at all; it's a pure cron-style trigger. - `src/app/api/aos/crm/companies/[id]/route.ts` — GET only (item 30 in the task list). - `src/app/api/aos/data-management/export/route.ts` — GET only (item 32). - `src/app/api/aos/settings/route.ts` — PATCH was already Zod-validated in an earlier task (`SETTING_SCHEMA = z.record(z.string(), z.string())` + `safeParse`); verified unchanged. Stage Summary: - `npx tsc --noEmit 2>&1 | grep "^src/" | wc -l` → **0**. No TypeScript errors introduced. The Zod v4 `z.record(K, V)` two-arg signature works correctly; the `z.string().startsWith(...)` chain works on `ZodString`; `safeParse(...).error.flatten()` types cleanly. - `bun run lint` → exit code **0**, no warnings/errors. - 24 files / 26 handlers now Zod-validate their input. Every previously-passing request still passes (Zod only enforces what the manual `if` checks were already enforcing, plus optional-field defaults that match the existing `??` fallbacks). Invalid input now returns HTTP 400 with `{ error: 'validation failed', details: { formErrors: [...], fieldErrors: { fieldName: [...] } } }` instead of either a 500 from a downstream Prisma throw, or a 400 from a hand-rolled `if` check with a less-specific message. - Next actions: none required. Possible follow-ups: (1) extract the duplicated `if (!parsed.success) return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })` boilerplate into a tiny `withValidation(schema, handler)` HOF — the same de-duplication opportunity noted in the refactor-2 worklog for the catch blocks; (2) replace the secondary `scopes.filter((s) => !ALL_SCOPES.includes(s))` / `events.filter((e) => !KNOWN_EVENTS.includes(e))` ad-hoc allow-list checks with proper Zod `z.enum(ALL_SCOPES)` / `z.enum(KNOWN_EVENTS)` once those arrays are declared `as const` — currently they're declared as plain `string[]` which doesn't satisfy `z.enum`'s `[string, ...string[]]` constraint.