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.
-
+
+
({
// 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 (
);
}
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 ? (
+
+ ) : null}
{gtmId ? (