diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 0000000..76fdb74 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": ".cursor/hooks/after-file-edit.sh", + "timeout": 180 + } + ], + "afterTabFileEdit": [ + { + "command": ".cursor/hooks/after-file-edit.sh", + "timeout": 180 + } + ], + "stop": [ + { + "command": ".cursor/hooks/stop-quality-check.sh", + "timeout": 600, + "loop_limit": 0 + } + ] + } +} diff --git a/.cursor/hooks/after-file-edit.sh b/.cursor/hooks/after-file-edit.sh new file mode 100755 index 0000000..cd8e797 --- /dev/null +++ b/.cursor/hooks/after-file-edit.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Formats the edited file and runs vitest related tests. +# Full format:check, lint, and test suite run on agent stop (stop-quality-check.sh). + +set -euo pipefail + +input=$(cat) +file_path=$( + printf '%s' "$input" | python3 -c "import sys, json; print(json.load(sys.stdin).get('file_path', ''))" +) + +if [[ -z "$file_path" || ! -f "$file_path" ]]; then + exit 0 +fi + +case "$file_path" in + *node_modules/* | */.next/* | */dist/* | */test-results/* | */playwright-report/*) + exit 0 + ;; +esac + +project_root="${CURSOR_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}" +cd "$project_root" + +log() { + echo "[cursor-hook:afterFileEdit] $*" >&2 +} + +format_file() { + case "$file_path" in + *.ts | *.tsx | *.js | *.jsx | *.json | *.css | *.md | *.mjs | *.cjs | *.yml | *.yaml) + log "Formatting $file_path" + bunx prettier --write "$file_path" + ;; + esac +} + +run_related_tests() { + case "$file_path" in + *.ts | *.tsx | *.js | *.jsx) + log "Running related tests for $file_path" + if ! bun run test -- related "$file_path" --run; then + log "Related tests failed for $file_path (see output above)" + fi + ;; + esac +} + +format_file +run_related_tests + +exit 0 diff --git a/.cursor/hooks/stop-quality-check.sh b/.cursor/hooks/stop-quality-check.sh new file mode 100755 index 0000000..ea07395 --- /dev/null +++ b/.cursor/hooks/stop-quality-check.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Full quality gate when the agent finishes a turn: format check, lint, unit tests. + +set -euo pipefail + +cat >/dev/null + +project_root="${CURSOR_PROJECT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)}" +cd "$project_root" + +log() { + echo "[cursor-hook:stop] $*" >&2 +} + +failures=() + +log "Running format:check..." +if ! bun run format:check; then + failures+=("format:check") +fi + +log "Running lint..." +if ! bun run lint; then + failures+=("lint") +fi + +log "Running unit tests..." +if ! bun run test -- --run; then + failures+=("test") +fi + +if ((${#failures[@]} > 0)); then + log "Quality gate failed: ${failures[*]}" + # Stop hooks are observational; exit 0 so the session is not blocked. + exit 0 +fi + +log "Quality gate passed." +exit 0 diff --git a/.eslintrc.json b/.eslintrc.json index 36eb852..7989c84 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,14 @@ { "extends": ["next/core-web-vitals", "next/typescript"], "rules": { - "@typescript-eslint/no-explicit-any": "warn" + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] } } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5979bce..040278d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -147,7 +147,8 @@ jobs: # # Environment secrets (GitHub "Production" vs "Preview"): only Appwrite targets differ per env: # APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_ID_TRIAL_SESSIONS, APPWRITE_COLLECTION_ID_AI_KEYS, APPWRITE_COLLECTION_ID_SUBSCRIPTIONS - # All other deploy secrets stay repository-wide: VERCEL_*, NEXT_PUBLIC_APPWRITE_*, APPWRITE_API_KEY. + # All other deploy secrets stay repository-wide: VERCEL_*, NEXT_PUBLIC_APPWRITE_*, APPWRITE_API_KEY, + # AI_PROVIDER, AI_API_KEY (Hosted AI / Ditectrev AI on server — synced to Vercel below). # Set GitHub variable NEXT_PUBLIC_SITE_URL (e.g. https://theopenstock.com) for canonical URLs, sitemap, and OG. # With `environment:` set, GitHub still exposes repository secrets to the job; environment adds/overrides # only the names defined on that environment (here: Appwrite DB + collection ids). @@ -188,6 +189,11 @@ jobs: vercel env add FINNHUB_BASE_URL $ENV_TYPE "" --value "${{ vars.FINNHUB_BASE_URL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} fi vercel env add FINNHUB_API_KEY $ENV_TYPE "" --value "${{ secrets.FINNHUB_API_KEY }}" --yes --force --sensitive --token=${{ secrets.VERCEL_TOKEN }} + vercel env add AI_PROVIDER $ENV_TYPE "" --value "${{ secrets.AI_PROVIDER }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + vercel env add AI_API_KEY $ENV_TYPE "" --value "${{ secrets.AI_API_KEY }}" --yes --force --sensitive --token=${{ secrets.VERCEL_TOKEN }} + if [ -n "${{ vars.AI_MODEL }}" ]; then + vercel env add AI_MODEL $ENV_TYPE "" --value "${{ vars.AI_MODEL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} + fi if [ -n "${{ vars.YAHOO_FINANCE_API_URL }}" ]; then vercel env add YAHOO_FINANCE_API_URL $ENV_TYPE "" --value "${{ vars.YAHOO_FINANCE_API_URL }}" --yes --force --token=${{ secrets.VERCEL_TOKEN }} fi diff --git a/.gitignore b/.gitignore index 0c57ca6..8ae2a21 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,9 @@ pnpm-lock.yaml # Testing /coverage *.lcov +/playwright-report/ +/test-results/ +/playwright/.cache/ # Vercel .vercel diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ae342af --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Build & dependencies +.next/ +out/ +node_modules/ +.vercel/ + +# Test & Playwright artifacts (generated locally / in CI) +coverage/ +playwright-report/ +test-results/ +.lighthouseci/ +e2e/**/*.png +**/*-snapshots/** + +# Lockfiles Prettier doesn't need to touch +bun.lockb diff --git a/README.md b/README.md index a28df1c..7e4cde4 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,16 @@ If `ollama serve` shows `127.0.0.1:11434: bind: address already in use`, Ollama Also required for trial features: `APPWRITE_COLLECTION_ID_TRIAL_SESSIONS`. +### Hosted AI (Ditectrev AI tier on production) + +Required on Vercel when subscribers use the **Hosted AI** plan. Synced from GitHub Actions on deploy (see `.github/workflows/deploy.yml`). + +| Variable | Description | Example | +| ------------- | ------------------------------------------------------------------------------------- | ---------- | +| `AI_PROVIDER` | Server LLM vendor for Hosted AI (`MISTRAL`, `OPENAI`, `GEMINI`, `DEEPSEEK`, `OLLAMA`) | `MISTRAL` | +| `AI_API_KEY` | API key for that provider | _(secret)_ | +| `AI_MODEL` | Optional model override; defaults are used per provider if unset | unset | + ### SEO & analytics (recommended for production) | Variable | Description | diff --git a/__tests__/performance.test.ts b/__tests__/performance.test.ts index cda78f4..baecde1 100644 --- a/__tests__/performance.test.ts +++ b/__tests__/performance.test.ts @@ -147,7 +147,9 @@ describe("Lazy loading behavior (Req 15.2)", () => { "AdBanner", "LazySection", "AIPredictionPanel", + "HomeHub", "StockOfTheDayPanel", + "ProductShell", ]; for (const imp of staticImports!) { diff --git a/app/(main)/home-page-client.tsx b/app/(main)/home-page-client.tsx index 1e5743d..64f33b7 100644 --- a/app/(main)/home-page-client.tsx +++ b/app/(main)/home-page-client.tsx @@ -1,7 +1,6 @@ "use client"; import dynamic from "next/dynamic"; -import Link from "next/link"; import { useState, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { @@ -17,12 +16,15 @@ import { } from "@/types"; import { SymbolHeader } from "@/components/SymbolHeader"; import { TabNavigation } from "@/components/TabNavigation"; +import { ProductGate } from "@/components/ProductShell"; import { LoadingSpinner } from "@/components/LoadingSpinner"; import { usePricingTier } from "@/lib/use-pricing-tier"; import { EXPLANATIONS_PROVIDER_CHANGED_EVENT } from "@/lib/explanation-provider"; import { fetchAIPredictionForCurrentProvider } from "@/lib/local-ollama-ai-prediction"; import { fetchStockOfTheDayForCurrentProvider } from "@/lib/local-ollama-stock-of-the-day"; +import { MARKET_UI_COPY } from "@/lib/market-ui-copy"; import { AIPredictionPanel } from "@/components/AIPredictionPanel"; +import { HomeHub } from "@/components/HomeHub"; import { StockOfTheDayPanel } from "@/components/StockOfTheDayPanel"; const OverviewTab = dynamic( () => import("@/components/OverviewTab").then((m) => m.OverviewTab), @@ -98,97 +100,6 @@ type TabType = | "forecasts" | "seasonals"; -const QUICK_LINKS = [ - { - id: "sectors", - label: "Sectors", - href: "/sectors", - description: "Compare sector performance", - icon: ( - - ), - }, - { - id: "heatmaps", - label: "Heatmaps", - href: "/heatmaps", - description: "Visual market overview", - icon: ( - - ), - }, - { - id: "screener", - label: "Screener", - href: "/screener", - description: "Filter and find assets", - icon: ( - - ), - }, - { - id: "calendars", - label: "Calendars", - href: "/calendars", - description: "Earnings, dividends & IPOs", - icon: ( - - ), - }, -] as const; - export function HomePageClient() { const router = useRouter(); const searchParams = useSearchParams(); @@ -263,7 +174,7 @@ export function HomePageClient() { const body = (await symbolResponse.json().catch(() => ({}))) as { error?: string; }; - throw new Error(body.error ?? "Failed to fetch symbol data"); + throw new Error(body.error ?? MARKET_UI_COPY.load.symbolData); } const symbolResult = await symbolResponse.json(); setSymbolData(symbolResult.data); @@ -272,7 +183,7 @@ export function HomePageClient() { `/api/market/historical/${selectedSymbol}?range=${timeRange}` ); if (!historicalResponse.ok) { - throw new Error("Failed to fetch historical data"); + throw new Error(MARKET_UI_COPY.load.historicalData); } const historicalResult = await historicalResponse.json(); setHistoricalData(historicalResult.data); @@ -311,7 +222,7 @@ export function HomePageClient() { } catch (err) { console.error("Error fetching symbol data:", err); setError( - err instanceof Error ? err.message : "Failed to load symbol data" + err instanceof Error ? err.message : MARKET_UI_COPY.load.symbolData ); } finally { setLoading(false); @@ -417,7 +328,7 @@ export function HomePageClient() { setAIPredictionError( error instanceof Error ? error.message - : "Failed to fetch AI prediction" + : MARKET_UI_COPY.load.aiPrediction ); } finally { setAIPredictionLoading(false); @@ -445,7 +356,7 @@ export function HomePageClient() { setStockOfTheDayError( error instanceof Error ? error.message - : "Failed to fetch stock of the day" + : MARKET_UI_COPY.load.stockOfTheDay ); } finally { setStockOfTheDayLoading(false); @@ -473,19 +384,17 @@ export function HomePageClient() { )} {error && !loading && ( -
-
-

- Error Loading Symbol -

-

{error}

- -
+
+
)} @@ -533,40 +442,25 @@ export function HomePageClient() { )} {!selectedSymbol && ( -
-
-
- {QUICK_LINKS.map((link) => ( - - - {link.icon} - - - {link.label} - - - {link.description} - - - ))} -
- - -
- -
- -
+
+ + router.push(`/?symbol=${encodeURIComponent(symbol)}`) + } + fearGreed={} + worldMarkets={} + stockOfTheDay={ + + } + />
)} diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 8210e54..cb18ae7 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,23 +1,38 @@ "use client"; import dynamic from "next/dynamic"; +import { Suspense } from "react"; import { Navigation } from "@/components/Navigation"; +import { HOME_PAGE_BACKGROUND } from "@/lib/home-ui"; const Footer = dynamic( () => import("@/components/Footer").then((m) => m.Footer), { ssr: false } ); +function NavigationFallback() { + return ( + + ); +} + export default function MainLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( -
- +
+ }> + + -
+
{children}
diff --git a/app/(main)/pricing/page.tsx b/app/(main)/pricing/page.tsx index cbbbe35..039ede9 100644 --- a/app/(main)/pricing/page.tsx +++ b/app/(main)/pricing/page.tsx @@ -5,6 +5,10 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { LoadingSpinner } from "@/components/LoadingSpinner"; import type { PricingTier, PricingTierInfo } from "@/types"; +import { AUTH_UI_COPY } from "@/lib/auth-ui-copy"; +import { DNA_BODY } from "@/lib/design-dna"; +import { MARKET_UP_TEXT } from "@/lib/market-semantics"; +import { MARKET_UI_COPY } from "@/lib/market-ui-copy"; const PricingPage = dynamic( () => import("@/components/PricingPage").then((m) => m.PricingPage), @@ -58,7 +62,7 @@ function PricingRouteContent() { } } catch { setMessage({ - text: "Failed to load pricing details. Please refresh.", + text: MARKET_UI_COPY.load.pricingDetails, tone: "warning", }); } @@ -90,9 +94,7 @@ function PricingRouteContent() { if (cancelled) return; if (!res.ok || !payload.success) { setMessage({ - text: - payload.error ?? - "Could not confirm your subscription. If you were charged, contact support.", + text: payload.error ?? AUTH_UI_COPY.subscriptionConfirmFailed, tone: "warning", }); return; @@ -107,7 +109,7 @@ function PricingRouteContent() { } catch { if (!cancelled) { setMessage({ - text: "Could not confirm checkout. Please refresh the page.", + text: AUTH_UI_COPY.checkoutConfirmFailed, tone: "warning", }); } @@ -147,14 +149,14 @@ function PricingRouteContent() { }; if (!response.ok || !payload.data?.url) { setMessage({ - text: payload.error ?? "Failed to start checkout.", + text: payload.error ?? MARKET_UI_COPY.load.checkout, tone: "warning", }); return; } window.location.assign(payload.data.url); } catch { - setMessage({ text: "Failed to start checkout.", tone: "warning" }); + setMessage({ text: MARKET_UI_COPY.load.checkout, tone: "warning" }); } }; @@ -167,9 +169,9 @@ function PricingRouteContent() { /> {message && (

diff --git a/app/(main)/stock-of-the-day/page.tsx b/app/(main)/stock-of-the-day/page.tsx index d816749..0182cc7 100644 --- a/app/(main)/stock-of-the-day/page.tsx +++ b/app/(main)/stock-of-the-day/page.tsx @@ -4,6 +4,12 @@ import { useEffect, useState } from "react"; import { usePricingTier } from "@/lib/use-pricing-tier"; import { EXPLANATIONS_PROVIDER_CHANGED_EVENT } from "@/lib/explanation-provider"; import { fetchStockOfTheDayForCurrentProvider } from "@/lib/local-ollama-stock-of-the-day"; +import { MARKET_UI_COPY } from "@/lib/market-ui-copy"; +import { + DNA_BODY_SECONDARY, + DNA_DISPLAY, + DNA_PAGE_STACK, +} from "@/lib/design-dna"; import { StockOfTheDayPanel } from "@/components/StockOfTheDayPanel"; import type { StockOfTheDayResult } from "@/types"; @@ -52,7 +58,7 @@ export default function StockOfTheDayPage() { } catch (err) { setItem(null); setLoadError( - err instanceof Error ? err.message : "Failed to load stock of the day" + err instanceof Error ? err.message : MARKET_UI_COPY.load.stockOfTheDay ); } finally { setLoading(false); @@ -88,13 +94,13 @@ export default function StockOfTheDayPage() { }, []); return ( -

-

- Stock of the day -

-

- AI-ranked daily opportunity across stocks and select liquid assets. -

+
+
+

Stock of the day

+

+ AI-ranked daily opportunity across stocks and select liquid assets. +

+
({ // Mock next/dynamic to render simple placeholders instead of lazy-loaded components vi.mock("next/dynamic", () => ({ __esModule: true, - default: (loader: () => Promise, opts?: any) => { - const MockComponent = (props: any) => ( + default: (_loader: () => Promise, _opts?: { ssr?: boolean }) => { + const MockComponent = (_props: Record) => (
); MockComponent.displayName = "DynamicMock"; @@ -53,7 +53,7 @@ vi.mock("next/dynamic", () => ({ })); vi.mock("@/components/SymbolHeader", () => ({ - SymbolHeader: ({ symbolData }: any) => ( + SymbolHeader: ({ symbolData }: { symbolData?: { name?: string } }) => (
{symbolData?.name}
), })); @@ -109,7 +109,7 @@ describe("Home Page", () => { beforeEach(() => { vi.clearAllMocks(); searchParamsState.symbol = null; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => ({ success: true, data: {} }), }); @@ -151,7 +151,7 @@ describe("Home Page", () => { describe("Symbol selection", () => { it("shows loading state when a symbol is in the URL", async () => { searchParamsState.symbol = "AAPL"; - (global.fetch as any).mockImplementation(() => new Promise(() => {})); + vi.mocked(global.fetch).mockImplementation(() => new Promise(() => {})); render(); await waitFor(() => { expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); @@ -160,13 +160,14 @@ describe("Home Page", () => { it("shows error state when symbol fetch fails", async () => { searchParamsState.symbol = "AAPL"; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: false, json: async () => ({ success: false }), }); render(); await waitFor(() => { - expect(screen.getByText("Error Loading Symbol")).toBeInTheDocument(); + expect(screen.getByTestId("symbol-load-error")).toBeInTheDocument(); + expect(screen.getByText("Couldn't load symbol")).toBeInTheDocument(); }); }); @@ -185,7 +186,7 @@ describe("Home Page", () => { lastUpdated: new Date().toISOString(), }; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => ({ success: true, data: mockSymbolData }), }); @@ -201,7 +202,7 @@ describe("Home Page", () => { it("has a Clear Selection button on error that returns to dashboard", async () => { searchParamsState.symbol = "AAPL"; - (global.fetch as any).mockResolvedValue({ + vi.mocked(global.fetch).mockResolvedValue({ ok: false, json: async () => ({ success: false }), }); @@ -209,10 +210,12 @@ describe("Home Page", () => { render(); await waitFor(() => { - expect(screen.getByText("Clear Selection")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Clear selection" }) + ).toBeInTheDocument(); }); - fireEvent.click(screen.getByText("Clear Selection")); + fireEvent.click(screen.getByRole("button", { name: "Clear selection" })); await waitFor(() => { expect( diff --git a/app/api/auth/email/send/route.ts b/app/api/auth/email/send/route.ts index 2961167..37c20a0 100644 --- a/app/api/auth/email/send/route.ts +++ b/app/api/auth/email/send/route.ts @@ -7,6 +7,7 @@ import { AppwriteException, ID } from "node-appwrite"; import { createServerClient } from "@/lib/appwrite"; import { getAppwriteServerEnv } from "@/lib/appwrite-server-env"; import { logger } from "@/lib/logger"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; export async function POST(request: NextRequest) { let body: unknown; @@ -59,7 +60,7 @@ export async function POST(request: NextRequest) { ? err.message : err instanceof Error ? err.message - : "Failed to send verification email."; + : MARKET_UI_COPY.auth.verificationEmail; if (typeof message === "string" && message.includes("sessions.write")) { message = "Appwrite API key must include the sessions.write scope. In Appwrite Console: Project → API keys → your key → Scopes → enable Sessions (write)."; diff --git a/app/api/calendar/dividends/route.ts b/app/api/calendar/dividends/route.ts index fbddeea..d18b13c 100644 --- a/app/api/calendar/dividends/route.ts +++ b/app/api/calendar/dividends/route.ts @@ -5,6 +5,7 @@ */ import { NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -23,10 +24,7 @@ export async function GET() { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch dividend events", + error: userFacingApiError(error, MARKET_UI_COPY.load.dividendCalendar), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/calendar/earnings/route.ts b/app/api/calendar/earnings/route.ts index 87835fe..4d0362c 100644 --- a/app/api/calendar/earnings/route.ts +++ b/app/api/calendar/earnings/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -27,10 +28,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch earnings events", + error: userFacingApiError(error, MARKET_UI_COPY.load.earningsCalendar), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/calendar/economic/route.ts b/app/api/calendar/economic/route.ts index 5e90448..754fe65 100644 --- a/app/api/calendar/economic/route.ts +++ b/app/api/calendar/economic/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -36,10 +37,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch economic events", + error: userFacingApiError(error, MARKET_UI_COPY.load.economicCalendar), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/calendar/ipos/route.ts b/app/api/calendar/ipos/route.ts index 498d526..2fc3eda 100644 --- a/app/api/calendar/ipos/route.ts +++ b/app/api/calendar/ipos/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -27,8 +28,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to fetch IPO events", + error: userFacingApiError(error, MARKET_UI_COPY.load.ipoCalendar), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/__tests__/symbol.test.ts b/app/api/market/__tests__/symbol.test.ts index ef39be3..6b7e5f1 100644 --- a/app/api/market/__tests__/symbol.test.ts +++ b/app/api/market/__tests__/symbol.test.ts @@ -147,7 +147,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/symbol/[symbol]", () => { it("should return symbol data with success shape", async () => { - (marketDataService.getSymbolData as any).mockResolvedValue( + vi.mocked(marketDataService.getSymbolData).mockResolvedValue( mockSymbolData ); @@ -164,7 +164,7 @@ describe("Successful Data Retrieval", () => { }); it("should call marketDataService.getSymbolData with correct symbol", async () => { - (marketDataService.getSymbolData as any).mockResolvedValue( + vi.mocked(marketDataService.getSymbolData).mockResolvedValue( mockSymbolData ); @@ -178,7 +178,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/historical/[symbol]", () => { it("should return historical data for valid symbol", async () => { - (marketDataService.getHistoricalPrices as any).mockResolvedValue( + vi.mocked(marketDataService.getHistoricalPrices).mockResolvedValue( mockHistoricalData ); @@ -197,7 +197,7 @@ describe("Successful Data Retrieval", () => { }); it("should pass range parameter to service", async () => { - (marketDataService.getHistoricalPrices as any).mockResolvedValue( + vi.mocked(marketDataService.getHistoricalPrices).mockResolvedValue( mockHistoricalData ); @@ -215,7 +215,7 @@ describe("Successful Data Retrieval", () => { }); it("should default range to 1Y when not provided", async () => { - (marketDataService.getHistoricalPrices as any).mockResolvedValue( + vi.mocked(marketDataService.getHistoricalPrices).mockResolvedValue( mockHistoricalData ); @@ -235,7 +235,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/fear-greed", () => { it("should return fear and greed data", async () => { - (marketDataService.getFearGreedIndex as any).mockResolvedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockResolvedValue( mockFearGreedData ); @@ -251,7 +251,7 @@ describe("Successful Data Retrieval", () => { }); it("should pass limit parameter", async () => { - (marketDataService.getFearGreedIndex as any).mockResolvedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockResolvedValue( mockFearGreedData ); @@ -267,7 +267,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/world-markets", () => { it("should return world markets data", async () => { - (marketDataService.getWorldMarkets as any).mockResolvedValue( + vi.mocked(marketDataService.getWorldMarkets).mockResolvedValue( mockWorldMarketsData ); @@ -284,7 +284,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/sectors", () => { it("should return sector performance data", async () => { - (marketDataService.getSectorPerformance as any).mockResolvedValue( + vi.mocked(marketDataService.getSectorPerformance).mockResolvedValue( mockSectorData ); @@ -299,7 +299,7 @@ describe("Successful Data Retrieval", () => { }); it("should pass period parameter to service", async () => { - (marketDataService.getSectorPerformance as any).mockResolvedValue( + vi.mocked(marketDataService.getSectorPerformance).mockResolvedValue( mockSectorData ); @@ -317,7 +317,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/indicators/[symbol]", () => { it("should return technical indicators", async () => { - (marketDataService.getTechnicalIndicators as any).mockResolvedValue( + vi.mocked(marketDataService.getTechnicalIndicators).mockResolvedValue( mockIndicatorsData ); @@ -337,7 +337,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/forecast/[symbol]", () => { it("should return forecast data", async () => { - (marketDataService.getForecastData as any).mockResolvedValue( + vi.mocked(marketDataService.getForecastData).mockResolvedValue( mockForecastData ); @@ -354,7 +354,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/seasonal/[symbol]", () => { it("should return seasonal pattern data", async () => { - (marketDataService.getSeasonalPatterns as any).mockResolvedValue( + vi.mocked(marketDataService.getSeasonalPatterns).mockResolvedValue( mockSeasonalData ); @@ -371,7 +371,7 @@ describe("Successful Data Retrieval", () => { describe("GET /api/market/financials/[symbol]", () => { it("should return financial data", async () => { - (marketDataService.getFinancials as any).mockResolvedValue( + vi.mocked(marketDataService.getFinancials).mockResolvedValue( mockFinancialsData ); @@ -402,7 +402,9 @@ describe("Caching Behavior", () => { it("should serve cached data when service returns cached result", async () => { // The service layer handles caching internally. When called twice, // the route should return the same data shape both times. - (marketDataService.getSymbolData as any).mockResolvedValue(mockSymbolData); + vi.mocked(marketDataService.getSymbolData).mockResolvedValue( + mockSymbolData + ); const { GET } = await import("@/app/api/market/symbol/[symbol]/route"); const req1 = makeRequest("http://localhost:3000/api/market/symbol/AAPL"); @@ -420,7 +422,7 @@ describe("Caching Behavior", () => { }); it("should include timestamp in every response for cache freshness", async () => { - (marketDataService.getFearGreedIndex as any).mockResolvedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockResolvedValue( mockFearGreedData ); @@ -435,7 +437,7 @@ describe("Caching Behavior", () => { }); it("should return consistent data shape across multiple calls", async () => { - (marketDataService.getWorldMarkets as any).mockResolvedValue( + vi.mocked(marketDataService.getWorldMarkets).mockResolvedValue( mockWorldMarketsData ); @@ -469,7 +471,7 @@ describe("Rate Limiting", () => { }); it("should propagate rate limit error from service as 502", async () => { - (marketDataService.getSymbolData as any).mockRejectedValue( + vi.mocked(marketDataService.getSymbolData).mockRejectedValue( new Error("Rate limit exceeded and no cached data available") ); @@ -484,7 +486,7 @@ describe("Rate Limiting", () => { }); it("should return 500 with rate limit error for fear-greed route", async () => { - (marketDataService.getFearGreedIndex as any).mockRejectedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockRejectedValue( new Error("Rate limit exceeded and no cached data available") ); @@ -499,7 +501,7 @@ describe("Rate Limiting", () => { }); it("should return 500 with rate limit error for world-markets route", async () => { - (marketDataService.getWorldMarkets as any).mockRejectedValue( + vi.mocked(marketDataService.getWorldMarkets).mockRejectedValue( new Error("Rate limit exceeded and no cached data available") ); @@ -514,7 +516,7 @@ describe("Rate Limiting", () => { }); it("should return 500 with rate limit error for sectors route", async () => { - (marketDataService.getSectorPerformance as any).mockRejectedValue( + vi.mocked(marketDataService.getSectorPerformance).mockRejectedValue( new Error("Rate limit exceeded and no cached data available") ); @@ -529,7 +531,7 @@ describe("Rate Limiting", () => { }); it("should return 500 with rate limit error for indicators route", async () => { - (marketDataService.getTechnicalIndicators as any).mockRejectedValue( + vi.mocked(marketDataService.getTechnicalIndicators).mockRejectedValue( new Error("Rate limit exceeded and no cached data available") ); @@ -555,7 +557,7 @@ describe("Error Responses", () => { describe("User-friendly error messages (Req 3.5, 14.2)", () => { it("should return descriptive error for fear-greed API failure", async () => { - (marketDataService.getFearGreedIndex as any).mockRejectedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockRejectedValue( new Error("Network timeout") ); @@ -571,7 +573,7 @@ describe("Error Responses", () => { }); it("should return descriptive error for world-markets API failure", async () => { - (marketDataService.getWorldMarkets as any).mockRejectedValue( + vi.mocked(marketDataService.getWorldMarkets).mockRejectedValue( new Error("Service unavailable") ); @@ -586,7 +588,7 @@ describe("Error Responses", () => { }); it("should return descriptive error for sectors API failure", async () => { - (marketDataService.getSectorPerformance as any).mockRejectedValue( + vi.mocked(marketDataService.getSectorPerformance).mockRejectedValue( new Error("External API error") ); @@ -601,7 +603,7 @@ describe("Error Responses", () => { }); it("should return descriptive error for indicators API failure", async () => { - (marketDataService.getTechnicalIndicators as any).mockRejectedValue( + vi.mocked(marketDataService.getTechnicalIndicators).mockRejectedValue( new Error("Failed to calculate indicators") ); @@ -615,11 +617,13 @@ describe("Error Responses", () => { expect(response.status).toBe(500); expect(data.success).toBe(false); - expect(data.error).toBe("Failed to calculate indicators"); + expect(data.error).toBe( + "We couldn't load technical indicators. Try again." + ); }); it("should return descriptive error for forecast API failure", async () => { - (marketDataService.getForecastData as any).mockRejectedValue( + vi.mocked(marketDataService.getForecastData).mockRejectedValue( new Error("Forecast data unavailable") ); @@ -634,7 +638,7 @@ describe("Error Responses", () => { }); it("should return descriptive error for seasonal API failure", async () => { - (marketDataService.getSeasonalPatterns as any).mockRejectedValue( + vi.mocked(marketDataService.getSeasonalPatterns).mockRejectedValue( new Error("Seasonal data unavailable") ); @@ -649,7 +653,7 @@ describe("Error Responses", () => { }); it("should return descriptive error for financials API failure", async () => { - (marketDataService.getFinancials as any).mockRejectedValue( + vi.mocked(marketDataService.getFinancials).mockRejectedValue( new Error("Financial data unavailable") ); @@ -669,7 +673,7 @@ describe("Error Responses", () => { describe("Non-Error object handling", () => { it("should return fallback message for non-Error throws in fear-greed", async () => { - (marketDataService.getFearGreedIndex as any).mockRejectedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockRejectedValue( "string error" ); @@ -680,11 +684,13 @@ describe("Error Responses", () => { expect(response.status).toBe(500); expect(data.success).toBe(false); - expect(data.error).toBe("Failed to fetch Fear & Greed Index"); + expect(data.error).toBe( + "We couldn't load the Fear & Greed index. Try again." + ); }); it("should return fallback message for non-Error throws in world-markets", async () => { - (marketDataService.getWorldMarkets as any).mockRejectedValue(42); + vi.mocked(marketDataService.getWorldMarkets).mockRejectedValue(42); const { GET } = await import("@/app/api/market/world-markets/route"); const req = makeRequest("http://localhost:3000/api/market/world-markets"); @@ -693,11 +699,11 @@ describe("Error Responses", () => { expect(response.status).toBe(500); expect(data.success).toBe(false); - expect(data.error).toBe("Failed to fetch world markets"); + expect(data.error).toBe("We couldn't load world markets. Try again."); }); it("should return fallback message for non-Error throws in sectors", async () => { - (marketDataService.getSectorPerformance as any).mockRejectedValue(null); + vi.mocked(marketDataService.getSectorPerformance).mockRejectedValue(null); const { GET } = await import("@/app/api/market/sectors/route"); const req = makeRequest("http://localhost:3000/api/market/sectors"); @@ -706,13 +712,15 @@ describe("Error Responses", () => { expect(response.status).toBe(500); expect(data.success).toBe(false); - expect(data.error).toBe("Failed to fetch sector performance"); + expect(data.error).toBe( + "We couldn't load sector performance. Try again." + ); }); }); describe("Symbol route live data errors", () => { it("should return a 502 when symbol API fails", async () => { - (marketDataService.getSymbolData as any).mockRejectedValue( + vi.mocked(marketDataService.getSymbolData).mockRejectedValue( new Error("API unavailable") ); @@ -727,7 +735,7 @@ describe("Error Responses", () => { }); it("should return a 502 when historical API fails", async () => { - (marketDataService.getHistoricalPrices as any).mockRejectedValue( + vi.mocked(marketDataService.getHistoricalPrices).mockRejectedValue( new Error("API unavailable") ); @@ -747,7 +755,7 @@ describe("Error Responses", () => { describe("Response shape consistency", () => { it("should always include success, data, and timestamp on success", async () => { - (marketDataService.getSectorPerformance as any).mockResolvedValue( + vi.mocked(marketDataService.getSectorPerformance).mockResolvedValue( mockSectorData ); @@ -762,7 +770,7 @@ describe("Error Responses", () => { }); it("should always include success, error, and timestamp on failure", async () => { - (marketDataService.getFearGreedIndex as any).mockRejectedValue( + vi.mocked(marketDataService.getFearGreedIndex).mockRejectedValue( new Error("Failure") ); diff --git a/app/api/market/ai-prediction/[symbol]/route.ts b/app/api/market/ai-prediction/[symbol]/route.ts index 5b69ee6..0a9956a 100644 --- a/app/api/market/ai-prediction/[symbol]/route.ts +++ b/app/api/market/ai-prediction/[symbol]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { aiMarketInsightsService } from "@/services/ai-market-insights.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; @@ -54,7 +55,8 @@ export async function GET( return NextResponse.json( { success: false, - error: "Configure an AI provider to generate predictions.", + error: + "AI is available on your plan, but this deployment has no active provider configuration.", }, { status: 503 } ); @@ -79,10 +81,10 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to generate AI prediction", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.aiPredictionGenerate + ), timestamp: new Date(), }, { status: 500 } @@ -151,10 +153,10 @@ export async function POST( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to validate AI prediction", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.aiPredictionValidate + ), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/ai-prediction/[symbol]/snapshot/route.ts b/app/api/market/ai-prediction/[symbol]/snapshot/route.ts index b069ebe..0a35fa1 100644 --- a/app/api/market/ai-prediction/[symbol]/snapshot/route.ts +++ b/app/api/market/ai-prediction/[symbol]/snapshot/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { aiMarketInsightsService } from "@/services/ai-market-insights.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; @@ -49,10 +50,10 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to load AI prediction snapshot", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.aiPredictionSnapshot + ), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/crypto/route.ts b/app/api/market/crypto/route.ts index 08e0273..e30ba1a 100644 --- a/app/api/market/crypto/route.ts +++ b/app/api/market/crypto/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -38,10 +39,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch crypto performance", + error: userFacingApiError(error, MARKET_UI_COPY.load.cryptoPerformance), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/etfs/route.ts b/app/api/market/etfs/route.ts index 25b362e..68fdb03 100644 --- a/app/api/market/etfs/route.ts +++ b/app/api/market/etfs/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -38,10 +39,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch ETF performance", + error: userFacingApiError(error, MARKET_UI_COPY.load.etfPerformance), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/fear-greed/route.ts b/app/api/market/fear-greed/route.ts index 87e6efe..5fdf305 100644 --- a/app/api/market/fear-greed/route.ts +++ b/app/api/market/fear-greed/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -28,10 +29,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch Fear & Greed Index", + error: userFacingApiError(error, MARKET_UI_COPY.load.fearGreed), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/financials/[symbol]/route.ts b/app/api/market/financials/[symbol]/route.ts index a223100..348deec 100644 --- a/app/api/market/financials/[symbol]/route.ts +++ b/app/api/market/financials/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -37,8 +38,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to fetch financials", + error: userFacingApiError(error, MARKET_UI_COPY.load.financials), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/forecast/[symbol]/route.ts b/app/api/market/forecast/[symbol]/route.ts index 3fd45eb..30ef6f1 100644 --- a/app/api/market/forecast/[symbol]/route.ts +++ b/app/api/market/forecast/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -37,10 +38,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch forecast data", + error: userFacingApiError(error, MARKET_UI_COPY.load.forecast), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/historical/[symbol]/route.ts b/app/api/market/historical/[symbol]/route.ts index a1ea2fa..4867f1d 100644 --- a/app/api/market/historical/[symbol]/route.ts +++ b/app/api/market/historical/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; import { TimeRange } from "@/types"; @@ -39,10 +40,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch live historical data", + error: userFacingApiError(error, MARKET_UI_COPY.load.liveHistorical), timestamp: new Date(), }, { status: 502 } diff --git a/app/api/market/indicators/[symbol]/route.ts b/app/api/market/indicators/[symbol]/route.ts index 4e6199e..99e10d8 100644 --- a/app/api/market/indicators/[symbol]/route.ts +++ b/app/api/market/indicators/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -37,10 +38,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch technical indicators", + error: userFacingApiError(error, MARKET_UI_COPY.load.indicators), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/search/__tests__/route.test.ts b/app/api/market/search/__tests__/route.test.ts index 0f97da2..6d48fc4 100644 --- a/app/api/market/search/__tests__/route.test.ts +++ b/app/api/market/search/__tests__/route.test.ts @@ -75,7 +75,7 @@ describe("GET /api/market/search", () => { }, ]; - (marketDataService.searchSymbols as any).mockResolvedValue(mockResults); + vi.mocked(marketDataService.searchSymbols).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/market/search?q=AAPL" @@ -91,7 +91,7 @@ describe("GET /api/market/search", () => { }); it("should return 500 on service error", async () => { - (marketDataService.searchSymbols as any).mockRejectedValue( + vi.mocked(marketDataService.searchSymbols).mockRejectedValue( new Error("Service error") ); @@ -117,7 +117,7 @@ describe("GET /api/market/search", () => { }, ]; - (marketDataService.searchSymbols as any).mockResolvedValue(mockResults); + vi.mocked(marketDataService.searchSymbols).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/market/search?q=AAPL" diff --git a/app/api/market/search/route.ts b/app/api/market/search/route.ts index 2e5f1d0..d536700 100644 --- a/app/api/market/search/route.ts +++ b/app/api/market/search/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -41,8 +42,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to search symbols", + error: userFacingApiError(error, MARKET_UI_COPY.search.failed), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/seasonal/[symbol]/route.ts b/app/api/market/seasonal/[symbol]/route.ts index e794973..199ae81 100644 --- a/app/api/market/seasonal/[symbol]/route.ts +++ b/app/api/market/seasonal/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -37,10 +38,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch seasonal patterns", + error: userFacingApiError(error, MARKET_UI_COPY.load.seasonal), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/sectors/route.ts b/app/api/market/sectors/route.ts index 0730d47..3b9aa48 100644 --- a/app/api/market/sectors/route.ts +++ b/app/api/market/sectors/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -38,10 +39,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch sector performance", + error: userFacingApiError(error, MARKET_UI_COPY.load.sectorHub), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/stock-of-the-day/route.ts b/app/api/market/stock-of-the-day/route.ts index 1ba3d02..d7bf0ef 100644 --- a/app/api/market/stock-of-the-day/route.ts +++ b/app/api/market/stock-of-the-day/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { aiMarketInsightsService } from "@/services/ai-market-insights.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; @@ -43,7 +44,7 @@ export async function GET(request: NextRequest) { success: false, error: resolved.error ?? - "Configure an AI explanations provider to generate dynamic stock-of-the-day picks.", + "AI is available on your plan, but this deployment has no active provider configuration for stock ideas.", timestamp: new Date(), }, { status: 400 } @@ -65,10 +66,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to load stock of the day", + error: userFacingApiError(error, MARKET_UI_COPY.load.stockOfTheDay), timestamp: new Date(), }, { status: 500 } @@ -132,10 +130,10 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to validate stock-of-the-day candidates", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.stockOfTheDayValidate + ), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/stocks/route.ts b/app/api/market/stocks/route.ts index e6b9190..82cb822 100644 --- a/app/api/market/stocks/route.ts +++ b/app/api/market/stocks/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -38,10 +39,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch stock performance", + error: userFacingApiError(error, MARKET_UI_COPY.load.stockPerformance), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/market/symbol/[symbol]/route.ts b/app/api/market/symbol/[symbol]/route.ts index a44f9fe..bdbb19c 100644 --- a/app/api/market/symbol/[symbol]/route.ts +++ b/app/api/market/symbol/[symbol]/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; @@ -36,10 +37,7 @@ export async function GET( return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch live symbol data", + error: userFacingApiError(error, MARKET_UI_COPY.load.liveSymbol), timestamp: new Date(), }, { status: 502 } diff --git a/app/api/market/world-markets/route.ts b/app/api/market/world-markets/route.ts index 24902b7..8d3a4fe 100644 --- a/app/api/market/world-markets/route.ts +++ b/app/api/market/world-markets/route.ts @@ -4,10 +4,11 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { marketDataService } from "@/services/market-data.service"; import { logger } from "@/lib/logger"; -export async function GET(request: NextRequest) { +export async function GET(_request: NextRequest) { try { const data = await marketDataService.getWorldMarkets(); @@ -22,10 +23,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to fetch world markets", + error: userFacingApiError(error, MARKET_UI_COPY.load.worldMarkets), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/screener/__tests__/route.test.ts b/app/api/screener/__tests__/route.test.ts index 4f3c79e..b1b26d1 100644 --- a/app/api/screener/__tests__/route.test.ts +++ b/app/api/screener/__tests__/route.test.ts @@ -104,7 +104,7 @@ describe("POST /api/screener/search", () => { }); it("should return results for valid filters", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/screener/search", @@ -133,8 +133,8 @@ describe("POST /api/screener/search", () => { }); it("should use preset filters when preset is specified", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); - (screenerService.getDefaultPresets as any).mockReturnValue(mockPresets); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue(mockPresets); const request = new NextRequest( "http://localhost:3000/api/screener/search", @@ -155,8 +155,8 @@ describe("POST /api/screener/search", () => { }); it("should use provided filters when preset is not found", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue([]); - (screenerService.getDefaultPresets as any).mockReturnValue(mockPresets); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue([]); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue(mockPresets); const filters = [ { field: "price", operator: "lt", value: 5, label: "Price < $5" }, @@ -176,7 +176,7 @@ describe("POST /api/screener/search", () => { }); it("should default to empty filters when none provided", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/screener/search", @@ -195,7 +195,7 @@ describe("POST /api/screener/search", () => { }); it("should pass multiple filters to service for AND logic (Req 26.8)", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue([ + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue([ mockResults[0], ]); @@ -231,8 +231,8 @@ describe("POST /api/screener/search", () => { }); it("should prefer preset filters over provided filters", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); - (screenerService.getDefaultPresets as any).mockReturnValue(mockPresets); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue(mockPresets); const request = new NextRequest( "http://localhost:3000/api/screener/search", @@ -255,7 +255,7 @@ describe("POST /api/screener/search", () => { }); it("should return 500 on service error", async () => { - (screenerService.fetchScreenerData as any).mockRejectedValue( + vi.mocked(screenerService.fetchScreenerData).mockRejectedValue( new Error("Service unavailable") ); @@ -276,7 +276,7 @@ describe("POST /api/screener/search", () => { }); it("should return 500 with generic message for non-Error throws", async () => { - (screenerService.fetchScreenerData as any).mockRejectedValue("unknown"); + vi.mocked(screenerService.fetchScreenerData).mockRejectedValue("unknown"); const request = new NextRequest( "http://localhost:3000/api/screener/search", @@ -290,7 +290,9 @@ describe("POST /api/screener/search", () => { const data = await response.json(); expect(response.status).toBe(500); - expect(data.error).toBe("Failed to search screener"); + expect(data.error).toBe( + "We couldn't run the screener. Check your filters and try again." + ); }); }); @@ -304,7 +306,7 @@ describe("GET /api/screener/presets", () => { }); it("should return default presets", async () => { - (screenerService.getDefaultPresets as any).mockReturnValue(mockPresets); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue(mockPresets); const response = await GET(); const data = await response.json(); @@ -316,7 +318,7 @@ describe("GET /api/screener/presets", () => { }); it("should return presets with required structure (Req 26.12)", async () => { - (screenerService.getDefaultPresets as any).mockReturnValue(mockPresets); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue(mockPresets); const response = await GET(); const data = await response.json(); @@ -333,7 +335,7 @@ describe("GET /api/screener/presets", () => { }); it("should return empty array when no presets exist", async () => { - (screenerService.getDefaultPresets as any).mockReturnValue([]); + vi.mocked(screenerService.getDefaultPresets).mockReturnValue([]); const response = await GET(); const data = await response.json(); @@ -344,7 +346,7 @@ describe("GET /api/screener/presets", () => { }); it("should return 500 on service error", async () => { - (screenerService.getDefaultPresets as any).mockImplementation(() => { + vi.mocked(screenerService.getDefaultPresets).mockImplementation(() => { throw new Error("Preset error"); }); @@ -533,7 +535,7 @@ describe("GET /api/screener/export", () => { }); it("should return CSV with correct headers", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/screener/export" @@ -556,7 +558,7 @@ describe("GET /api/screener/export", () => { }); it("should include result data in CSV rows", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/screener/export" @@ -572,7 +574,7 @@ describe("GET /api/screener/export", () => { }); it("should apply filters from query params", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue([]); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue([]); const filters = JSON.stringify([ { field: "price", operator: "gt", value: 100, label: "Price > $100" }, @@ -603,7 +605,7 @@ describe("GET /api/screener/export", () => { }); it("should return 500 on service error", async () => { - (screenerService.fetchScreenerData as any).mockRejectedValue( + vi.mocked(screenerService.fetchScreenerData).mockRejectedValue( new Error("Export failed") ); @@ -620,7 +622,7 @@ describe("GET /api/screener/export", () => { }); it("should return header-only CSV when no results match", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue([]); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue([]); const request = new NextRequest( "http://localhost:3000/api/screener/export" @@ -649,7 +651,7 @@ describe("GET /api/screener/export", () => { matchScore: 0, }, ]; - (screenerService.fetchScreenerData as any).mockResolvedValue( + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue( resultWithoutOptionals ); @@ -685,7 +687,7 @@ describe("GET /api/screener/export", () => { matchScore: 0, }, ]; - (screenerService.fetchScreenerData as any).mockResolvedValue( + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue( resultWithQuotes ); @@ -700,7 +702,7 @@ describe("GET /api/screener/export", () => { }); it("should export with no filters when param is absent", async () => { - (screenerService.fetchScreenerData as any).mockResolvedValue(mockResults); + vi.mocked(screenerService.fetchScreenerData).mockResolvedValue(mockResults); const request = new NextRequest( "http://localhost:3000/api/screener/export" diff --git a/app/api/screener/export/route.ts b/app/api/screener/export/route.ts index 564704c..7e49671 100644 --- a/app/api/screener/export/route.ts +++ b/app/api/screener/export/route.ts @@ -4,6 +4,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { screenerService } from "@/services/screener.service"; import { logger } from "@/lib/logger"; import type { ScreenerFilter, ScreenerResult } from "@/types"; @@ -87,10 +88,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to export screener results", + error: userFacingApiError(error, MARKET_UI_COPY.load.screenerExport), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/screener/presets/route.ts b/app/api/screener/presets/route.ts index 85223d1..354432a 100644 --- a/app/api/screener/presets/route.ts +++ b/app/api/screener/presets/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { screenerService } from "@/services/screener.service"; import { logger } from "@/lib/logger"; import type { ScreenerFilter, ScreenerPreset } from "@/types"; @@ -27,8 +28,10 @@ export async function GET() { return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to fetch presets", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.screenerPresetsLoad + ), timestamp: new Date(), }, { status: 500 } @@ -79,7 +82,10 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to save preset", + error: userFacingApiError( + error, + MARKET_UI_COPY.load.screenerPresetsSave + ), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/screener/search/route.ts b/app/api/screener/search/route.ts index 4e277b0..d73b0a3 100644 --- a/app/api/screener/search/route.ts +++ b/app/api/screener/search/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { screenerService } from "@/services/screener.service"; import { logger } from "@/lib/logger"; import type { ScreenerFilter } from "@/types"; @@ -43,8 +44,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to search screener", + error: userFacingApiError(error, MARKET_UI_COPY.load.screenerResults), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/stripe/confirm-checkout-session/route.ts b/app/api/stripe/confirm-checkout-session/route.ts index 449b034..0479719 100644 --- a/app/api/stripe/confirm-checkout-session/route.ts +++ b/app/api/stripe/confirm-checkout-session/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { stripeBillingService } from "@/services/stripe-billing.service"; @@ -33,10 +34,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to confirm checkout session", + error: userFacingApiError(error, MARKET_UI_COPY.load.confirmCheckout), }, { status: 400 } ); diff --git a/app/api/stripe/create-checkout-session/route.ts b/app/api/stripe/create-checkout-session/route.ts index 8caa196..012c740 100644 --- a/app/api/stripe/create-checkout-session/route.ts +++ b/app/api/stripe/create-checkout-session/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import type { PricingTier } from "@/types"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { stripeBillingService } from "@/services/stripe-billing.service"; @@ -44,7 +45,7 @@ export async function POST(request: NextRequest) { if (!session.url) { return NextResponse.json( - { success: false, error: "Failed to create checkout URL" }, + { success: false, error: MARKET_UI_COPY.load.checkoutUrl }, { status: 500 } ); } @@ -58,10 +59,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to create checkout session", + error: userFacingApiError(error, MARKET_UI_COPY.load.checkout), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/stripe/create-portal-session/route.ts b/app/api/stripe/create-portal-session/route.ts index 1dbb2c7..1754d75 100644 --- a/app/api/stripe/create-portal-session/route.ts +++ b/app/api/stripe/create-portal-session/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { stripeBillingService } from "@/services/stripe-billing.service"; @@ -33,10 +34,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to create billing portal session", + error: userFacingApiError(error, MARKET_UI_COPY.billing.portalFailed), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/stripe/webhook/route.ts b/app/api/stripe/webhook/route.ts index d9d1983..5c1d38c 100644 --- a/app/api/stripe/webhook/route.ts +++ b/app/api/stripe/webhook/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import Stripe from "stripe"; import { stripeBillingService } from "@/services/stripe-billing.service"; @@ -59,10 +60,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to process Stripe webhook", + error: userFacingApiError(error, MARKET_UI_COPY.account.stripeWebhook), }, { status: 500 } ); diff --git a/app/api/subscription/cancel/route.ts b/app/api/subscription/cancel/route.ts index 0805670..b71c82b 100644 --- a/app/api/subscription/cancel/route.ts +++ b/app/api/subscription/cancel/route.ts @@ -9,6 +9,7 @@ import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; import { stripeBillingService } from "@/services/stripe-billing.service"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; export async function DELETE(request: NextRequest) { try { @@ -46,7 +47,7 @@ export async function DELETE(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to cancel Stripe subscription", + error: MARKET_UI_COPY.billing.cancelFailed, timestamp: new Date(), }, { status: 500 } @@ -67,7 +68,7 @@ export async function DELETE(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to cancel subscription", + error: MARKET_UI_COPY.billing.cancelFailed, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/subscription/current/route.ts b/app/api/subscription/current/route.ts index 3727b81..97af66c 100644 --- a/app/api/subscription/current/route.ts +++ b/app/api/subscription/current/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; import { getAuthenticatedUser } from "@/lib/server-auth"; @@ -47,7 +48,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to fetch current subscription", + error: MARKET_UI_COPY.account.subscription, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/subscription/downgrade/route.ts b/app/api/subscription/downgrade/route.ts index 3283848..ede2511 100644 --- a/app/api/subscription/downgrade/route.ts +++ b/app/api/subscription/downgrade/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; import { PricingTier } from "@/types"; @@ -62,7 +63,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to downgrade subscription", + error: MARKET_UI_COPY.account.downgrade, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/subscription/subscribe/route.ts b/app/api/subscription/subscribe/route.ts index 0ef7f1b..d400841 100644 --- a/app/api/subscription/subscribe/route.ts +++ b/app/api/subscription/subscribe/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; import { PricingTier } from "@/types"; @@ -69,7 +70,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to create subscription", + error: MARKET_UI_COPY.account.subscribe, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/subscription/tiers/route.ts b/app/api/subscription/tiers/route.ts index 6c9c63e..2985009 100644 --- a/app/api/subscription/tiers/route.ts +++ b/app/api/subscription/tiers/route.ts @@ -5,6 +5,7 @@ */ import { NextResponse } from "next/server"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; @@ -23,7 +24,7 @@ export async function GET() { return NextResponse.json( { success: false, - error: "Failed to fetch pricing tiers", + error: MARKET_UI_COPY.load.pricingDetails, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/subscription/upgrade/route.ts b/app/api/subscription/upgrade/route.ts index fc4aea3..945fe68 100644 --- a/app/api/subscription/upgrade/route.ts +++ b/app/api/subscription/upgrade/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY } from "@/lib/api-user-error"; import { subscriptionService } from "@/services/subscription.service"; import { logger } from "@/lib/logger"; import { PricingTier } from "@/types"; @@ -69,7 +70,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json( { success: false, - error: "Failed to upgrade subscription", + error: MARKET_UI_COPY.account.upgrade, timestamp: new Date(), }, { status: 500 } diff --git a/app/api/trial/eligibility/route.ts b/app/api/trial/eligibility/route.ts index ef44bce..e3050c4 100644 --- a/app/api/trial/eligibility/route.ts +++ b/app/api/trial/eligibility/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { parseTrialIdentity } from "@/lib/trial-request-identity"; import { serverTrialManagementService } from "@/services/server-trial-management.service"; import { logger } from "@/lib/logger"; @@ -26,10 +27,10 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to check trial eligibility", + error: userFacingApiError( + error, + MARKET_UI_COPY.account.trialEligibility + ), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/trial/end/route.ts b/app/api/trial/end/route.ts index 06eebdf..d1382f2 100644 --- a/app/api/trial/end/route.ts +++ b/app/api/trial/end/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { parseTrialIdentity } from "@/lib/trial-request-identity"; import { serverTrialManagementService } from "@/services/server-trial-management.service"; import { logger } from "@/lib/logger"; @@ -31,10 +32,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to end trial session", + error: userFacingApiError(error, MARKET_UI_COPY.account.trialEnd), timestamp: new Date(), }, { status: 500 } diff --git a/app/api/trial/start/route.ts b/app/api/trial/start/route.ts index 59b6605..6b484db 100644 --- a/app/api/trial/start/route.ts +++ b/app/api/trial/start/route.ts @@ -1,10 +1,11 @@ /** * POST /api/trial/start - * Starts a new 15-minute trial session. + * Starts a new trial session. * Requirements: 21.1, 21.12 */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { parseTrialIdentity } from "@/lib/trial-request-identity"; import { serverTrialManagementService } from "@/services/server-trial-management.service"; import { logger } from "@/lib/logger"; @@ -31,10 +32,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error - ? error.message - : "Failed to start trial session", + error: userFacingApiError(error, MARKET_UI_COPY.account.trialStart), timestamp: new Date(), }, { status: 403 } diff --git a/app/api/trial/status/route.ts b/app/api/trial/status/route.ts index 84344b6..b0c071f 100644 --- a/app/api/trial/status/route.ts +++ b/app/api/trial/status/route.ts @@ -5,6 +5,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { MARKET_UI_COPY, userFacingApiError } from "@/lib/api-user-error"; import { parseTrialIdentity } from "@/lib/trial-request-identity"; import { serverTrialManagementService } from "@/services/server-trial-management.service"; import { logger } from "@/lib/logger"; @@ -25,8 +26,7 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Failed to get trial status", + error: userFacingApiError(error, MARKET_UI_COPY.account.trialStatus), timestamp: new Date(), }, { status: 500 } diff --git a/app/auth/callback/google/page.tsx b/app/auth/callback/google/page.tsx index 4d5752e..6410e4a 100644 --- a/app/auth/callback/google/page.tsx +++ b/app/auth/callback/google/page.tsx @@ -82,7 +82,7 @@ export default function GoogleOAuthCallbackPage() { return (
-

{message}

+

{message}

); } diff --git a/app/globals.css b/app/globals.css index bb2f95a..011a40f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,14 +3,14 @@ @tailwind utilities; :root { - --background: #ffffff; - --foreground: #171717; + --background: #fafaf9; + --foreground: #1c1917; } @media (prefers-color-scheme: dark) { :root { - --background: #0a0a0a; - --foreground: #ededed; + --background: #0c0a09; + --foreground: #f5f5f4; } } diff --git a/app/layout.tsx b/app/layout.tsx index 5407e91..4419f28 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import Script from "next/script"; import { Ibarra_Real_Nova, Merriweather } from "next/font/google"; import { JsonLd } from "@/components/JsonLd"; import { buildRootMetadata } from "@/lib/site-seo"; @@ -31,18 +32,6 @@ export default function RootLayout({ return ( - {gtmId ? ( - + ) : null} {gtmId ? (