/** * BankBot API client — typed fetch wrapper for all backend endpoints. * Handles auth headers, token refresh, and error normalization. * * HF Spaces mode: when NEXT_PUBLIC_API_URL is empty, all /api/* calls * go to the same origin — Nginx routes them to FastAPI internally. * WebSocket also uses the same origin with ws:// or wss://. */ // In HF mode NEXT_PUBLIC_API_URL is "" — use empty string so fetch uses same origin const API_BASE = process.env.NEXT_PUBLIC_API_URL || ""; // ─── WebSocket URL derivation ───────────────────────────────────────────────── // HF mode (API_BASE=""): derive from window.location at runtime // External mode: convert http→ws, https→wss function getWsBase(): string { if (typeof window === "undefined") return ""; if (!API_BASE) { // Same-origin WebSocket — use current page protocol const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; return `${proto}//${window.location.host}`; } return API_BASE.replace(/^https/, "wss").replace(/^http/, "ws"); } // ─── Token management (browser-only) ───────────────────────────────────────── export const tokenStore = { getAccess: (): string | null => typeof window !== "undefined" ? localStorage.getItem("bb_access_token") : null, getRefresh: (): string | null => typeof window !== "undefined" ? localStorage.getItem("bb_refresh_token") : null, setTokens: (access: string, refresh?: string) => { if (typeof window === "undefined") return; localStorage.setItem("bb_access_token", access); if (refresh) localStorage.setItem("bb_refresh_token", refresh); }, clear: () => { if (typeof window === "undefined") return; localStorage.removeItem("bb_access_token"); localStorage.removeItem("bb_refresh_token"); localStorage.removeItem("bb_user"); }, }; // ─── Core fetch wrapper ─────────────────────────────────────────────────────── async function apiFetch( path: string, options: RequestInit = {}, retry = true ): Promise { const token = tokenStore.getAccess(); const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); // Auto-refresh on 401 if (res.status === 401 && retry) { const refreshToken = tokenStore.getRefresh(); if (refreshToken) { try { const refreshRes = await fetch(`${API_BASE}/api/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (refreshRes.ok) { const data = await refreshRes.json(); tokenStore.setTokens(data.access_token); return apiFetch(path, options, false); } } catch { // refresh failed — clear tokens } } tokenStore.clear(); throw new ApiError(401, "Session expired. Please log in again."); } if (!res.ok) { let detail = `HTTP ${res.status}`; try { const err = await res.json(); detail = err.detail || detail; } catch { /* ignore */ } throw new ApiError(res.status, detail); } return res.json() as Promise; } export class ApiError extends Error { constructor(public status: number, message: string) { super(message); this.name = "ApiError"; } } // ─── Auth ───────────────────────────────────────────────────────────────────── export interface LoginResponse { access_token: string; refresh_token: string; token_type: string; user_id: string; name: string; email: string; } export const authApi = { login: (email: string, password: string) => { const form = new URLSearchParams({ username: email, password }); return apiFetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: form.toString(), }); }, register: (email: string, password: string, name: string) => apiFetch("/api/auth/register", { method: "POST", body: JSON.stringify({ email, password, name }), }), me: () => apiFetch<{ user_id: string; email: string; name: string; financial_personality: string; notifications_enabled?: boolean; ai_coaching_enabled?: boolean; fraud_alerts_enabled?: boolean; }>("/api/auth/me"), updateSettings: (data: { name?: string; financial_personality?: string; notifications_enabled?: boolean; ai_coaching_enabled?: boolean; fraud_alerts_enabled?: boolean; }) => apiFetch<{ user_id: string; email: string; name: string; financial_personality: string; notifications_enabled?: boolean; ai_coaching_enabled?: boolean; fraud_alerts_enabled?: boolean; }>("/api/auth/settings", { method: "PATCH", body: JSON.stringify(data) }), logout: () => { tokenStore.clear(); return Promise.resolve(); }, }; export interface FinancialGoal { id: string; title: string; target_amount: number; current_amount: number; progress_percent: number; target_date: string | null; days_left: number | null; monthly_contribution?: number; months_remaining?: number; on_track: boolean; } export const goalsApi = { list: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ goals: FinancialGoal[]; summary: { count: number; total_target: number; total_saved: number; overall_progress: number }; }>(`/api/goals${qs}`); }, contribute: (goalId: string, amount: number, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ id: string; current_amount: number; progress_percent: number; completed: boolean }>( `/api/goals/${goalId}/contribute${qs}`, { method: "POST", body: JSON.stringify({ amount }) } ); }, }; export const loansApi = { eligibility: (data: { salary: number; credit_score: number; existing_loans: number; employment_years: number; age: number; loan_amount: number; }, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ approval_probability: number; approval_status: string; risk_level: string; loan_score: number; emi: number; monthly_emi: number; recommendations: string[]; issues: string[]; comparison: Array<{ rate: string; tenure: string; emi: number; total_amount: number; interest: number }>; }>(`/api/loans/eligibility${qs}`, { method: "POST", body: JSON.stringify(data) }); }, }; // ─── Dashboard ──────────────────────────────────────────────────────────────── export interface DashboardOverview { total_balance: number; accounts: Array<{ id: string; type: string; balance: number; currency: string }>; monthly_income: number; monthly_expenses: number; savings_rate: number; spending_by_category: Array<{ name: string; value: number }>; recent_transactions: Array<{ id: string; merchant: string; category: string; amount: number; type: string; timestamp: string; }>; cash_flow: Array<{ month: string; income: number; expenses: number; savings: number }>; health_score: number; fraud_alert_count: number; ai_briefing: { summary?: string; briefing?: string; insights?: string[] }; } export const dashboardApi = { overview: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch(`/api/dashboard/overview${qs}`); }, }; // ─── AI ─────────────────────────────────────────────────────────────────────── export interface HealthScore { overall_score: number; categories: Record; } export interface ForecastResult { monthly_projections?: Array<{ month: number; balance: number; savings: number }>; scenarios?: { conservative: number; expected: number; optimistic: number }; } export const aiApi = { healthScore: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch(`/api/ai/coaching/score${qs}`); }, briefing: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ summary: string; insights: string[] }>(`/api/ai/coaching/briefing${qs}`); }, behaviorInsights: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ insights: string[]; metrics: Record }>(`/api/ai/behavior/insights${qs}`); }, twinPredict: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch(`/api/ai/twin/predict${qs}`); }, twinFuture: (months = 12, userId?: string) => { const qs = new URLSearchParams({ months: String(months), ...(userId ? { user_id: userId } : {}) }); return apiFetch(`/api/ai/twin/future?${qs}`); }, twinScenarios: (months = 6, userId?: string) => { const qs = new URLSearchParams({ months: String(months), ...(userId ? { user_id: userId } : {}) }); return apiFetch<{ scenarios: Record }>(`/api/ai/twin/scenarios?${qs}`); }, fraudAnalysis: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ alerts: Array<{ id: string; risk_score: number; details: string }> }>(`/api/ai/fraud/analysis${qs}`); }, subscriptions: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ subscriptions: Array<{ merchant: string; amount: number; recommendation: string }> }>(`/api/ai/subscriptions/optimize${qs}`); }, chat: (message: string, userId?: string, sessionId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ response: string; session_id: string }>(`/api/ai/chat${qs}`, { method: "POST", body: JSON.stringify({ message, session_id: sessionId }), }); }, chatSessions: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ sessions: Array<{ id: string; title: string; created_at: string | null; updated_at: string | null; message_count: number; preview: string; }>; count: number; }>(`/api/ai/chat/sessions${qs}`); }, createChatSession: (userId?: string, title = "New chat") => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ id: string; title: string; created_at: string | null; updated_at: string | null; message_count: number; preview: string; }>(`/api/ai/chat/sessions${qs}`, { method: "POST", body: JSON.stringify({ title }), }); }, deleteChatSession: (sessionId: string, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ ok: boolean }>(`/api/ai/chat/sessions/${sessionId}${qs}`, { method: "DELETE" }); }, chatHistory: (sessionId: string, userId?: string, limit = 100) => { const params = new URLSearchParams({ session_id: sessionId, limit: String(limit) }); if (userId) params.set("user_id", userId); return apiFetch<{ session_id: string; messages: Array<{ id: string; role: "user" | "assistant"; content: string; created_at: string | null }>; count: number; }>(`/api/ai/chat/history?${params}`); }, clearChatHistory: (sessionId: string, userId?: string) => { const params = new URLSearchParams({ session_id: sessionId }); if (userId) params.set("user_id", userId); return apiFetch<{ ok: boolean; message: string; session_id: string }>( `/api/ai/chat/history?${params}`, { method: "DELETE" } ); }, simulatePurchase: (amount: number, merchant: string, category: string, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch>(`/api/ai/simulate/purchase${qs}`, { method: "POST", body: JSON.stringify({ amount, merchant, category }), }); }, // ── Coach Mode ────────────────────────────────────────────────────────────── weeklyCoaching: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ generated_at: string; week_start: string; week_end: string; health_score: number; week_spend: number; week_income: number; spend_delta_pct: number; top_categories: Array<{ name: string; amount: number }>; anomalies: string[]; coaching_report: string; improvements: string[]; }>(`/api/ai/coach/weekly${qs}`); }, // ── Fraud Explanation ─────────────────────────────────────────────────────── fraudExplain: (fraudLogId: string) => apiFetch<{ fraud_log_id: string; transaction_id: string; merchant: string; amount: number; timestamp: string; risk_score: number; raw_reasons: string[]; ai_explanation: string; user_avg_amount: number; amount_vs_avg: number; }>(`/api/ai/fraud/explain/${fraudLogId}`), // ── Monthly Narrative ─────────────────────────────────────────────────────── monthlyNarrative: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ month: string; generated_at: string; summary: { total_spend: number; total_income: number; spend_delta_pct: number; savings_balance: number; investment_value: number; investment_gain_pct: number; monthly_subscriptions: number; }; category_changes: Array<{ category: string; this_month: number; last_month: number; delta_pct: number; }>; narrative: string; }>(`/api/ai/narrative/monthly${qs}`); }, }; // ─── Notifications ──────────────────────────────────────────────────────────── export interface NotificationItem { id: string; title: string; message: string; type: "alert" | "insight" | "warning" | string; read: boolean; created_at: string; } export const notificationsApi = { list: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ notifications: NotificationItem[]; unread_count: number }>(`/api/notifications/${qs}`); }, markRead: (id: string, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ success: boolean }>(`/api/notifications/${id}/read${qs}`, { method: "PATCH" }); }, markAllRead: (userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ success: boolean }>(`/api/notifications/read-all${qs}`, { method: "PATCH" }); }, dismiss: (id: string, userId?: string) => { const qs = userId ? `?user_id=${userId}` : ""; return apiFetch<{ success: boolean }>(`/api/notifications/${id}${qs}`, { method: "DELETE" }); }, }; // ─── Transactions ───────────────────────────────────────────────────────────── export const transactionsApi = { list: (params: { page?: number; limit?: number; category?: string; type?: string; userId?: string } = {}) => { const qs = new URLSearchParams(); if (params.page) qs.set("page", String(params.page)); if (params.limit) qs.set("limit", String(params.limit)); if (params.category) qs.set("category", params.category); if (params.type) qs.set("type", params.type); if (params.userId) qs.set("user_id", params.userId); return apiFetch<{ transactions: Array<{ id: string; merchant: string; category: string; amount: number; type: string; timestamp: string }>; total: number; page: number; pages: number; }>(`/api/transactions/?${qs}`); }, }; // ─── Payments ───────────────────────────────────────────────────────────────── export interface Payment { id: string; user_id: string; amount: number; currency: string; recipient_name: string; recipient_account: string; payment_type: string; status: "pending" | "completed" | "failed" | "flagged"; risk_score: number; fraud_flag: boolean; created_at: string; transaction_reference: string | null; note: string | null; ai_insight: string | null; } export interface PaymentHistoryResponse { payments: Payment[]; total: number; page: number; pages: number; stats: { total_sent: number; total_received: number; flagged_count: number; }; } export const paymentsApi = { create: (data: { amount: number; currency?: string; recipient_name: string; recipient_account: string; payment_type?: string; note?: string; }) => apiFetch("/api/payments/create", { method: "POST", body: JSON.stringify(data), }), transfer: (data: { amount: number; to_account_type: string; note?: string }) => apiFetch("/api/payments/transfer", { method: "POST", body: JSON.stringify(data), }), history: (params: { page?: number; limit?: number; payment_type?: string; status?: string; } = {}) => { const qs = new URLSearchParams(); if (params.page) qs.set("page", String(params.page)); if (params.limit) qs.set("limit", String(params.limit)); if (params.payment_type) qs.set("payment_type", params.payment_type); if (params.status) qs.set("status", params.status); return apiFetch(`/api/payments/history?${qs}`); }, get: (id: string) => apiFetch(`/api/payments/${id}`), verify: (payment_id: string, confirm: boolean) => apiFetch<{ message: string; payment: Payment }>("/api/payments/verify", { method: "POST", body: JSON.stringify({ payment_id, confirm }), }), cancel: (id: string) => apiFetch<{ message: string }>(`/api/payments/${id}`, { method: "DELETE" }), }; // ─── WebSocket factory ──────────────────────────────────────────────────────── export function createChatWebSocket(userId?: string): WebSocket { const wsBase = getWsBase(); const qs = userId ? `?user_id=${userId}` : ""; return new WebSocket(`${wsBase}/api/ai/chat/ws${qs}`); } // ─── Status ─────────────────────────────────────────────────────────────────── export const statusApi = { check: () => apiFetch<{ ai_backend: string; ai_available: boolean; db_type: string; cache_type: string }>("/api/status"), }; // ─── Memory ─────────────────────────────────────────────────────────────────── export interface ChatHistoryMessage { id: string; session_id: string | null; role: "user" | "assistant"; content: string; created_at: string; } export interface ChatSession { id: string; title: string; created_at: string; updated_at: string; } export const memoryApi = { history: (sessionId?: string) => { const qs = sessionId ? `?session_id=${sessionId}` : ""; return apiFetch<{ messages: ChatHistoryMessage[]; sessions: ChatSession[]; total: number }>(`/api/memory/history${qs}`); }, save: (data: { session_id?: string; role: string; content: string; session_title?: string }) => apiFetch("/api/memory/save", { method: "POST", body: JSON.stringify(data) }), clear: (sessionId?: string) => { const qs = sessionId ? `?session_id=${sessionId}` : ""; return apiFetch<{ deleted: number }>(`/api/memory/clear${qs}`, { method: "DELETE" }); }, getPreferences: () => apiFetch<{ theme: string; language: string }>("/api/memory/preferences"), updatePreferences: (data: { theme?: string; language?: string }) => apiFetch<{ theme: string; language: string }>("/api/memory/preferences", { method: "PATCH", body: JSON.stringify(data), }), }; // ─── Documents ──────────────────────────────────────────────────────────────── export interface DocumentRecord { id: string; filename: string; file_type: string; file_size: number; summary: string | null; insights: string[]; extracted_length: number; created_at: string; } export interface DocumentDetail extends DocumentRecord { messages: Array<{ id: string; role: string; content: string; language: string; created_at: string }>; } export const documentsApi = { upload: (file: File, language = "en") => { const form = new FormData(); form.append("file", file); const token = tokenStore.getAccess(); return fetch(`${API_BASE}/api/documents/upload?language=${language}`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: form, }).then(async (res) => { if (!res.ok) { const err = await res.json().catch(() => ({})); throw new ApiError(res.status, err.detail || `HTTP ${res.status}`); } return res.json() as Promise; }); }, history: () => apiFetch<{ documents: DocumentRecord[] }>("/api/documents/history"), get: (id: string) => apiFetch(`/api/documents/${id}`), chat: (id: string, question: string, language = "en") => apiFetch<{ question: string; answer: string; document_id: string; language: string }>( `/api/documents/chat/${id}`, { method: "POST", body: JSON.stringify({ question, language }) } ), analyze: (id: string, language = "en") => apiFetch<{ id: string; summary: string; insights: string[]; suspicious: string[] }>( `/api/documents/analyze/${id}?language=${language}`, { method: "POST" } ), delete: (id: string) => apiFetch<{ message: string }>(`/api/documents/${id}`, { method: "DELETE" }), };