server.js
  1  require('dotenv').config();
  2  const express = require('express');
  3  const cors = require('cors');
  4  const OpenAI = require('openai');
  5  const { Client } = require('@notionhq/client');
  6  
  7  const app = express();
  8  const PORT = process.env.PORT || 5000;
  9  
 10  // Middleware
 11  app.use(cors());
 12  app.use(express.json());
 13  
 14  // Initialize Notion client
 15  const notion = new Client({
 16    auth: process.env.NOTION_API_KEY,
 17  });
 18  
 19  const DATABASE_ID = process.env.NOTION_DATABASE_ID;
 20  
 21  // Initialize OpenAI client
 22  const openai = new OpenAI({
 23    apiKey: process.env.OPENAI_API_KEY
 24  });
 25  
 26  // Cache for descriptions to avoid repeated LLM calls (persists across requests)
 27  const descriptionCache = new Map();
 28  
 29  // Cache for API responses based on date range: { 'start-end': { ledger: [], summary: {}, timestamp: Date } }
 30  const apiCache = new Map();
 31  const CACHE_DURATION = 1000 * 60 * 60; // 1 hour cache duration
 32  
 33  // Category classification keywords
 34  const CATEGORY_KEYWORDS = {
 35    'Income1': ['income1'],
 36    'Income2': ['income2'],
 37    'Income3': ['income3'],
 38    'Income4': ['income4'],
 39    'Income5': ['income5'],
 40    'Food & Dining': ['grocery', 'geocery', 'starbucks', 'coffee', 'stsrbucks', 'dinner', 'lunch', 'cafe', 'wee', 'trade joe', 'h mart', 'lamian', 'ice cream', 'snacks', 'dish', 'gum', 'mocha', 'purple drinks', 'ice coffee'],
 41    'Transportation': ['uber', 'bus', 'subway', 'ticket', 'air', 'ny-boston', 'travel-taxi', 'travel bus'],
 42    'Shopping': ['shein', 'scarf', 'socks', 'case', 'clothes', 'cloth', 'decoration', 'tv', 'mount', 'stand', 'spray', 'mop', 'glue', 'file', 'headphone', 'iphone', 'laptop', 'laplop', 'mask', 'lighting', 'plant', 'battery', 'map', 'item'],
 43    'Entertainment': ['tennis', 'card', 'big head', 'headshot', 'boost', 'swimming', 'pool'],
 44    'Health & Wellness': ['yoga', 'nail', 'tail', 'white', 'body'],
 45    'Utilities & Bills': ['spectrum', 'equipment', 'membership', 'cursor', 'ieee', 'mint', 'mobile', 'apple care', 'ai tools'],
 46    'Travel': ['travel', 'visa', 'hotel', 'airbnb', 'chicago', 'boston', 'ohio', 'back home', 'chicago to boston'],
 47    'Services': ['logistic', 'application', 'opt', 'editing', 'set up', 'mounting'],
 48    'Other': ['daily expense', 'expense']
 49  };
 50  
 51  // Classify description into category (keyword-based, fast)
 52  function classifyDescription(description) {
 53    if (!description) return 'Uncategorized';
 54    
 55    const desc = description.toLowerCase();
 56    
 57    // First check for Income categories (more specific)
 58    if (desc.includes('income1')) return 'Income1';
 59    if (desc.includes('income2')) return 'Income2';
 60    if (desc.includes('income3')) return 'Income3';
 61    if (desc.includes('income4')) return 'Income4';
 62    if (desc.includes('income5')) return 'Income5';
 63    
 64    // Then check other categories
 65    for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
 66      // Skip Income categories as already checked above
 67      if (category.startsWith('Income')) continue;
 68      
 69      for (const keyword of keywords) {
 70        if (desc.includes(keyword)) {
 71          return category;
 72        }
 73      }
 74    }
 75    
 76    return 'Uncategorized';
 77  }
 78  
 79  // Smart LLM-based classification (for unique/unknown descriptions)
 80  async function classifyWithLLM(description) {
 81    try {
 82      const response = await openai.chat.completions.create({
 83        model: 'gpt-4o-mini',
 84        messages: [
 85          {
 86            role: 'system',
 87            content: `You are a financial transaction classifier. Categorize the description into one of: 
 88  Income1, Income2, Income3, Income4, Income5, Food & Dining, Transportation, Shopping, Entertainment, 
 89  Health & Wellness, Utilities & Bills, Travel, Services, or Other. 
 90  Respond with ONLY the category name, nothing else.`
 91          },
 92          {
 93            role: 'user',
 94            content: `Categorize: "${description}"`
 95          }
 96        ],
 97        temperature: 0.3,
 98        max_tokens: 20
 99      });
100      
101      const category = response.choices[0].message.content.trim();
102      return category;
103    } catch (error) {
104      console.error('LLM classification error:', error);
105      return 'Uncategorized';
106    }
107  }
108  
109  // Helper function to fetch ALL pages from Notion (handles pagination)
110  async function fetchAllPages(query) {
111    const allPages = [];
112    let cursor = undefined;
113    
114    do {
115      const queryWithCursor = { ...query };
116      if (cursor) {
117        queryWithCursor.start_cursor = cursor;
118      }
119      
120      const response = await notion.databases.query(queryWithCursor);
121      allPages.push(...response.results);
122      cursor = response.next_cursor;
123    } while (cursor);
124    
125    return allPages;
126  }
127  
128  // API Routes
129  
130  // Helper function to get cache key
131  function getCacheKey(start, end) {
132    return `${start || 'all'}-${end || 'all'}`;
133  }
134  
135  // Helper function to check if cache is valid
136  function isCacheValid(cacheEntry) {
137    if (!cacheEntry || !cacheEntry.timestamp) return false;
138    const age = Date.now() - cacheEntry.timestamp;
139    return age < CACHE_DURATION;
140  }
141  
142  // Get all ledger entries with optional time filtering
143  app.get('/api/ledger', async (req, res) => {
144    try {
145      const { start, end, forceRefresh } = req.query;
146      const cacheKey = getCacheKey(start, end);
147      
148      // Check cache first (unless forceRefresh is true)
149      if (forceRefresh !== 'true') {
150        const cached = apiCache.get(cacheKey);
151        if (cached && isCacheValid(cached)) {
152          console.log(`✅ Cache hit for ${cacheKey}`);
153          return res.json(cached.ledger);
154        }
155      }
156  
157      console.log(`🔄 Fetching from Notion for ${cacheKey}`);
158      
159      // Build query with optional date filter
160      const query = {
161        database_id: DATABASE_ID,
162        sorts: [
163          {
164            property: 'Date',
165            direction: 'descending',
166          },
167        ],
168      };
169  
170      // Add date filter if provided
171      if (start || end) {
172        query.filter = {
173          and: [],
174        };
175        
176        if (start) {
177          query.filter.and.push({
178            property: 'Date',
179            date: {
180              on_or_after: start,
181            },
182          });
183        }
184        
185        if (end) {
186          query.filter.and.push({
187            property: 'Date',
188            date: {
189              on_or_before: end,
190            },
191          });
192        }
193      }
194  
195      // Fetch ALL pages with pagination
196      const allPages = await fetchAllPages(query);
197  
198      const entries = await Promise.all(allPages.map(async (page) => {
199        const properties = page.properties;
200        const description = properties.Description?.title?.[0]?.plain_text || '';
201        
202        // Always use LLM to classify (bypass Notion's Category field completely)
203        let category;
204        if (descriptionCache.has(description)) {
205          // Use cached result (fast)
206          category = descriptionCache.get(description);
207        } else {
208          // Call LLM for new/unknown descriptions only
209          category = await classifyWithLLM(description);
210          descriptionCache.set(description, category);
211        }
212        
213        const payment = properties.Payment?.number || 0;
214        const deposit = properties.Deposit?.number || 0;
215        
216        // Determine Type: 
217        // Income = Deposit has value AND Payment is empty/0
218        // Expense = Payment has value (even if Deposit also has value)
219        const type = (deposit > 0 && payment === 0) ? 'Income' : 'Expense';
220        const amount = (deposit > 0 && payment === 0) ? deposit : payment;
221        
222        return {
223          id: page.id,
224          date: properties.Date?.date?.start || '',
225          description: description,
226          payment: payment,
227          deposit: deposit,
228          amount: amount,
229          type: type,
230          account: properties.Account?.select?.name || '',
231          category: category,
232        };
233      }));
234  
235      // Cache the result
236      apiCache.set(cacheKey, {
237        ledger: entries,
238        timestamp: Date.now()
239      });
240  
241      res.json(entries);
242    } catch (error) {
243      console.error('Error fetching ledger:', error);
244      res.status(500).json({ error: 'Failed to fetch ledger entries' });
245    }
246  });
247  
248  // Get categories summary (supports time filtering via query params)
249  app.get('/api/summary', async (req, res) => {
250    try {
251      const { start, end, forceRefresh } = req.query;
252      const cacheKey = `summary-${getCacheKey(start, end)}`;
253      
254      // Check cache first (unless forceRefresh is true)
255      if (forceRefresh !== 'true') {
256        const cached = apiCache.get(cacheKey);
257        if (cached && isCacheValid(cached)) {
258          console.log(`✅ Cache hit for ${cacheKey}`);
259          return res.json(cached.summary);
260        }
261      }
262  
263      console.log(`🔄 Fetching summary from Notion for ${cacheKey}`);
264      
265      // Build query with optional date filter
266      const query = {
267        database_id: DATABASE_ID,
268      };
269  
270      if (start || end) {
271        query.filter = { and: [] };
272        
273        if (start) {
274          query.filter.and.push({
275            property: 'Date',
276            date: { on_or_after: start },
277          });
278        }
279        
280        if (end) {
281          query.filter.and.push({
282            property: 'Date',
283            date: { on_or_before: end },
284          });
285        }
286      }
287  
288      // Fetch ALL pages with pagination
289      const allPages = await fetchAllPages(query);
290  
291      const summary = {};
292      
293      for (const page of allPages) {
294        const properties = page.properties;
295        const description = properties.Description?.title?.[0]?.plain_text || '';
296        
297        // Always use LLM to classify (bypass Notion's Category field completely)
298        let category;
299        if (descriptionCache.has(description)) {
300          category = descriptionCache.get(description);
301        } else {
302          category = await classifyWithLLM(description);
303          descriptionCache.set(description, category);
304        }
305        
306        const payment = properties.Payment?.number || 0;
307        const deposit = properties.Deposit?.number || 0;
308        
309        // Determine Type: 
310        // Income = Deposit has value AND Payment is empty/0
311        // Expense = Payment has value (even if Deposit also has value)
312        const isIncome = (deposit > 0 && payment === 0);
313        const amount = isIncome ? deposit : payment;
314  
315        if (!summary[category]) {
316          summary[category] = {
317            total: 0,
318            count: 0,
319            income: 0,
320            expense: 0,
321          };
322        }
323  
324        summary[category].total += amount;
325        summary[category].count += 1;
326        
327        if (isIncome) {
328          summary[category].income += deposit;
329        } else if (payment > 0) {
330          summary[category].expense += payment;
331        }
332      }
333  
334      // Cache the result
335      apiCache.set(cacheKey, {
336        summary: summary,
337        timestamp: Date.now()
338      });
339  
340      res.json(summary);
341    } catch (error) {
342      console.error('Error fetching summary:', error);
343      res.status(500).json({ error: 'Failed to fetch summary' });
344    }
345  });
346  
347  // Add new entry
348  app.post('/api/ledger', async (req, res) => {
349    try {
350      // Clear cache when new entry is added
351      apiCache.clear();
352      console.log('🔄 Cache cleared after adding new entry');
353      
354      const { date, description, payment, account, category } = req.body;
355  
356      // Auto-classify if category not provided
357      const finalCategory = category || classifyDescription(description);
358  
359      const response = await notion.pages.create({
360        parent: {
361          database_id: DATABASE_ID,
362        },
363        properties: {
364          Date: {
365            date: {
366              start: date,
367            },
368          },
369          Description: {
370            title: [
371              {
372                text: {
373                  content: description,
374                },
375              },
376            ],
377          },
378          Payment: {
379            number: payment,
380          },
381          Account: {
382            select: {
383              name: account,
384            },
385          },
386          Category: {
387            select: {
388              name: finalCategory,
389            },
390          },
391        },
392      });
393  
394      res.json({ success: true, id: response.id, category: finalCategory });
395    } catch (error) {
396      console.error('Error creating entry:', error);
397      res.status(500).json({ error: 'Failed to create entry' });
398    }
399  });
400  
401  // Get time-based analysis (daily, weekly, monthly)
402  app.get('/api/analysis/time', async (req, res) => {
403    try {
404      const allPages = await fetchAllPages({ database_id: DATABASE_ID });
405  
406      const daily = {};
407      const weekly = {};
408      const monthly = {};
409      
410      allPages.forEach(page => {
411        const properties = page.properties;
412        const dateStr = properties.Date?.date?.start;
413        if (!dateStr) return;
414        
415        const date = new Date(dateStr);
416        const payment = properties.Payment?.number || 0;
417        
418        // Daily
419        const dayKey = date.toISOString().split('T')[0];
420        daily[dayKey] = (daily[dayKey] || 0) + payment;
421        
422        // Weekly
423        const weekStart = new Date(date);
424        weekStart.setDate(date.getDate() - date.getDay());
425        const weekKey = weekStart.toISOString().split('T')[0];
426        weekly[weekKey] = (weekly[weekKey] || 0) + payment;
427        
428        // Monthly
429        const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
430        monthly[monthKey] = (monthly[monthKey] || 0) + payment;
431      });
432  
433      res.json({ daily, weekly, monthly });
434    } catch (error) {
435      console.error('Error fetching time analysis:', error);
436      res.status(500).json({ error: 'Failed to fetch time analysis' });
437    }
438  });
439  
440  // Get account-based analysis
441  app.get('/api/analysis/account', async (req, res) => {
442    try {
443      const allPages = await fetchAllPages({ database_id: DATABASE_ID });
444  
445      const accounts = {};
446      
447      allPages.forEach(page => {
448        const properties = page.properties;
449        const account = properties.Account?.select?.name || 'Other';
450        const payment = properties.Payment?.number || 0;
451  
452        if (!accounts[account]) {
453          accounts[account] = {
454            total: 0,
455            count: 0,
456          };
457        }
458  
459        accounts[account].total += payment;
460        accounts[account].count += 1;
461      });
462  
463      res.json(accounts);
464    } catch (error) {
465      console.error('Error fetching account analysis:', error);
466      res.status(500).json({ error: 'Failed to fetch account analysis' });
467    }
468  });
469  
470  // Get merchant/description analysis
471  app.get('/api/analysis/merchant', async (req, res) => {
472    try {
473      const allPages = await fetchAllPages({ database_id: DATABASE_ID });
474  
475      const merchants = {};
476      
477      allPages.forEach(page => {
478        const properties = page.properties;
479        const description = properties.Description?.title?.[0]?.plain_text || '';
480        const payment = properties.Payment?.number || 0;
481  
482        if (!merchants[description]) {
483          merchants[description] = {
484            total: 0,
485            count: 0,
486          };
487        }
488  
489        merchants[description].total += payment;
490        merchants[description].count += 1;
491      });
492  
493      // Sort by total descending
494      const sorted = Object.entries(merchants)
495        .map(([name, data]) => ({ name, ...data }))
496        .sort((a, b) => b.total - a.total)
497        .slice(0, 20); // Top 20
498  
499      res.json(sorted);
500    } catch (error) {
501      console.error('Error fetching merchant analysis:', error);
502      res.status(500).json({ error: 'Failed to fetch merchant analysis' });
503    }
504  });
505  
506  // Get trends (daily spending trend)
507  app.get('/api/analysis/trends', async (req, res) => {
508    try {
509      const allPages = await fetchAllPages({
510        database_id: DATABASE_ID,
511        sorts: [{ property: 'Date', direction: 'ascending' }],
512      });
513  
514      const dailyTrend = [];
515      let runningTotal = 0;
516      
517      allPages.forEach(page => {
518        const properties = page.properties;
519        const dateStr = properties.Date?.date?.start;
520        const payment = properties.Payment?.number || 0;
521        
522        if (dateStr) {
523          runningTotal += payment;
524          dailyTrend.push({
525            date: dateStr,
526            payment,
527            cumulative: runningTotal,
528          });
529        }
530      });
531  
532      res.json(dailyTrend);
533    } catch (error) {
534      console.error('Error fetching trends:', error);
535      res.status(500).json({ error: 'Failed to fetch trends' });
536    }
537  });
538  
539  // Get balance analysis (income vs expense)
540  app.get('/api/analysis/balance', async (req, res) => {
541    try {
542      const allPages = await fetchAllPages({ database_id: DATABASE_ID });
543  
544      let totalIncome = 0;
545      let totalExpense = 0;
546      
547      allPages.forEach(page => {
548        const properties = page.properties;
549        const payment = properties.Payment?.number || 0;
550        const deposit = properties.Deposit?.number || 0;
551        
552        // Income = Deposit has value AND Payment is empty/0
553        // Expense = Payment has value (even if Deposit also has value)
554        const isIncome = (deposit > 0 && payment === 0);
555        
556        if (isIncome) {
557          totalIncome += deposit;
558        } else if (payment > 0) {
559          totalExpense += payment;
560        }
561      });
562  
563      res.json({
564        income: totalIncome,
565        expense: totalExpense,
566        balance: totalIncome - totalExpense,
567      });
568    } catch (error) {
569      console.error('Error fetching balance analysis:', error);
570      res.status(500).json({ error: 'Failed to fetch balance analysis' });
571    }
572  });
573  
574  // Get all available categories
575  app.get('/api/categories', (req, res) => {
576    const categories = Object.keys(CATEGORY_KEYWORDS);
577    res.json(categories);
578  });
579  
580  // Classify a description (uses keyword matching, then LLM if needed)
581  app.post('/api/classify', async (req, res) => {
582    try {
583      const { description } = req.body;
584      if (!description) {
585        return res.status(400).json({ error: 'Description is required' });
586      }
587      
588      // Try keyword matching first
589      let category = classifyDescription(description);
590      
591      // If uncategorized, try LLM
592      if (category === 'Uncategorized') {
593        category = await classifyWithLLM(description);
594      }
595      
596      res.json({ description, category });
597    } catch (error) {
598      console.error('Error classifying:', error);
599      res.status(500).json({ error: 'Failed to classify description' });
600    }
601  });
602  
603  // Analyze all descriptions and return discovered categories (LLM-powered)
604  app.get('/api/analyze-categories', async (req, res) => {
605    try {
606      const allPages = await fetchAllPages({ database_id: DATABASE_ID });
607  
608      const uniqueDescriptions = new Set();
609      allPages.forEach(page => {
610        const description = page.properties.Description?.title?.[0]?.plain_text || '';
611        if (description) uniqueDescriptions.add(description);
612      });
613  
614      const descriptions = Array.from(uniqueDescriptions);
615      
616      // Use LLM to analyze and categorize
617      const categories = {};
618      for (const desc of descriptions) {
619        const category = await classifyWithLLM(desc);
620        if (!categories[category]) {
621          categories[category] = [];
622        }
623        categories[category].push(desc);
624      }
625  
626      res.json({ 
627        totalDescriptions: descriptions.length,
628        uniqueCategories: Object.keys(categories).length,
629        categories: categories 
630      });
631    } catch (error) {
632      console.error('Error analyzing categories:', error);
633      res.status(500).json({ error: 'Failed to analyze categories' });
634    }
635  });
636  
637  // Batch update categories for all uncategorized entries
638  app.post('/api/update-categories', async (req, res) => {
639    try {
640      const response = await notion.databases.query({
641        database_id: DATABASE_ID,
642      });
643  
644      const updates = [];
645      let successCount = 0;
646      let errorCount = 0;
647  
648      for (const page of response.results) {
649        const properties = page.properties;
650        const currentCategory = properties.Category?.select?.name;
651        const description = properties.Description?.title?.[0]?.plain_text || '';
652        
653        // Only update if uncategorized
654        if (currentCategory === 'Uncategorized' || !currentCategory) {
655          const newCategory = classifyDescription(description);
656          
657          if (newCategory && newCategory !== 'Uncategorized') {
658            try {
659              await notion.pages.update({
660                page_id: page.id,
661                properties: {
662                  Category: {
663                    select: {
664                      name: newCategory,
665                    },
666                  },
667                },
668              });
669              successCount++;
670            } catch (error) {
671              console.error(`Error updating page ${page.id}:`, error);
672              errorCount++;
673            }
674          }
675        }
676      }
677  
678      res.json({
679        success: true,
680        updated: successCount,
681        errors: errorCount,
682        message: `Successfully updated ${successCount} entries. ${errorCount} errors.`
683      });
684    } catch (error) {
685      console.error('Error updating categories:', error);
686      res.status(500).json({ error: 'Failed to update categories' });
687    }
688  });
689  
690  // AI Insights - Generate comprehensive spending insights
691  app.get('/api/ai-insights', async (req, res) => {
692    try {
693      const { start, end } = req.query;
694      
695      // Build query with optional date filter
696      const query = {
697        database_id: DATABASE_ID,
698        sorts: [{ property: 'Date', direction: 'descending' }],
699      };
700  
701      if (start || end) {
702        query.filter = { and: [] };
703        if (start) {
704          query.filter.and.push({ property: 'Date', date: { on_or_after: start } });
705        }
706        if (end) {
707          query.filter.and.push({ property: 'Date', date: { on_or_before: end } });
708        }
709      }
710  
711      const allPages = await fetchAllPages(query);
712  
713      // Process ledger entries
714      const entries = [];
715      for (const page of allPages) {
716        const properties = page.properties;
717        const description = properties.Description?.title?.[0]?.plain_text || '';
718        
719        let category;
720        if (descriptionCache.has(description)) {
721          category = descriptionCache.get(description);
722        } else {
723          category = await classifyWithLLM(description);
724          descriptionCache.set(description, category);
725        }
726        
727        const payment = properties.Payment?.number || 0;
728        const deposit = properties.Deposit?.number || 0;
729        const isIncome = (deposit > 0 && payment === 0);
730        const amount = isIncome ? deposit : payment;
731        
732        entries.push({
733          date: properties.Date?.date?.start || '',
734          description,
735          category,
736          amount,
737          type: isIncome ? 'Income' : 'Expense',
738          account: properties.Account?.select?.name || '',
739        });
740      }
741  
742      // Calculate statistics
743      const totalIncome = entries.filter(e => e.type === 'Income').reduce((sum, e) => sum + e.amount, 0);
744      const totalExpense = entries.filter(e => e.type === 'Expense').reduce((sum, e) => sum + e.amount, 0);
745      const avgDailyExpense = totalExpense / Math.max(1, new Set(entries.map(e => e.date?.split('T')[0])).size);
746      
747      // Category statistics
748      const categoryStats = {};
749      entries.filter(e => e.type === 'Expense').forEach(e => {
750        if (!categoryStats[e.category]) {
751          categoryStats[e.category] = { total: 0, count: 0, avg: 0 };
752        }
753        categoryStats[e.category].total += e.amount;
754        categoryStats[e.category].count += 1;
755      });
756      Object.keys(categoryStats).forEach(cat => {
757        categoryStats[cat].avg = categoryStats[cat].total / categoryStats[cat].count;
758      });
759  
760      // Top spending categories
761      const topCategories = Object.entries(categoryStats)
762        .sort((a, b) => b[1].total - a[1].total)
763        .slice(0, 5)
764        .map(([cat, stats]) => ({ category: cat, ...stats }));
765  
766      // Detect anomalies (spending > 3x average in that category)
767      const anomalies = entries.filter(e => {
768        if (e.type !== 'Expense' || !categoryStats[e.category]) return false;
769        return e.amount > categoryStats[e.category].avg * 3;
770      });
771  
772      // Prepare data for LLM
773      const context = {
774        period: start && end ? `${start} to ${end}` : 'all time',
775        totalIncome,
776        totalExpense,
777        balance: totalIncome - totalExpense,
778        avgDailyExpense,
779        categories: topCategories,
780        anomalies: anomalies.length,
781        totalEntries: entries.length,
782      };
783  
784      // Generate AI insights using OpenAI
785      const systemPrompt = `You are a financial advisor analyzing spending data. Provide:
786  1. A concise 2-3 sentence spending summary
787  2. List 3-5 actionable recommendations to improve financial health
788  3. Highlight 1-2 unusual spending patterns if any
789  
790  Be friendly, specific, and data-driven.`;
791      
792      const userPrompt = `Analyze this spending data from ${context.period}:
793  - Total Income: $${context.totalIncome.toFixed(2)}
794  - Total Expenses: $${context.totalExpense.toFixed(2)}
795  - Balance: $${context.balance.toFixed(2)}
796  - Average Daily Spending: $${context.avgDailyExpense.toFixed(2)}
797  - Top Spending Categories: ${topCategories.map(c => `${c.category} ($${c.total.toFixed(2)})`).join(', ')}
798  - Anomalies detected: ${anomalies.length} transactions
799  - Total entries: ${context.totalEntries}
800  
801  ${anomalies.length > 0 ? `Notable unusual expenses:\n${anomalies.slice(0, 5).map(a => `- ${a.description}: $${a.amount.toFixed(2)} in ${a.category}`).join('\n')}` : ''}
802  
803  Provide your analysis and recommendations.`;
804  
805      const llmResponse = await openai.chat.completions.create({
806        model: 'gpt-4o-mini',
807        messages: [
808          { role: 'system', content: systemPrompt },
809          { role: 'user', content: userPrompt }
810        ],
811        temperature: 0.7,
812        max_tokens: 500
813      });
814  
815      const aiAnalysis = llmResponse.choices[0].message.content.trim();
816  
817      res.json({
818        summary: {
819          totalIncome,
820          totalExpense,
821          balance: totalIncome - totalExpense,
822          avgDailyExpense,
823          totalEntries: entries.length,
824          period: context.period,
825        },
826        topCategories,
827        anomalies: anomalies.slice(0, 10),
828        aiInsights: aiAnalysis,
829        categoryStats,
830      });
831    } catch (error) {
832      console.error('Error generating AI insights:', error);
833      res.status(500).json({ error: 'Failed to generate AI insights' });
834    }
835  });
836  
837  // Health check
838  app.get('/api/health', (req, res) => {
839    res.json({ status: 'ok' });
840  });
841  
842  // Export for Vercel serverless
843  module.exports = app;
844  
845  // Only listen in development
846  if (process.env.NODE_ENV !== 'production') {
847    app.listen(PORT, () => {
848      console.log(`Server running on port ${PORT}`);
849    });
850  }
851