// TaxPack Receipt Utilities - Pure functions for extraction, export, and summarization export interface Receipt { id: string; merchant: string; amount: number; currency: string; date: string; category: string; paymentMethod: string; emailSubject: string; source: 'gmail'; } /** * Extract receipt data from a Gmail message */ export function extractReceiptFromEmail(message: any): Receipt | null { if (!message || !message.payload) return null; const headers = message.payload.headers || []; const subject = headers.find((h: any) => h.name === 'Subject')?.value || ''; const from = headers.find((h: any) => h.name === 'From')?.value || ''; const dateHeader = headers.find((h: any) => h.name === 'Date')?.value || ''; // Get email body let body = ''; const getBody = (part: any): string => { if (part.body?.data) { try { return atob(part.body.data.replace(/-/g, '+').replace(/_/g, '/')); } catch { return ''; } } if (part.parts) { for (const p of part.parts) { const b = getBody(p); if (b) return b; } } return ''; }; body = getBody(message.payload); // Extract amount using regex patterns const amountPatterns = [ /(?:total|amount|charged|paid|payment)[\s:]*[$€£]\s*([\d,]+\.?\d*)/i, /[$€£]\s*([\d,]+\.?\d*)\s*(?:total|usd|eur|gbp)/i, /(?:order total|grand total)[\s:]*[$€£]?\s*([\d,]+\.?\d*)/i, /(?:price|cost)[\s:]*[$€£]?\s*([\d,]+\.?\d*)/i, /\$\s*([\d,]+\.\d{2})/, /([\d,]+\.\d{2})\s*(?:USD|EUR|GBP)/i, ]; let amount = 0; for (const pattern of amountPatterns) { const match = body.match(pattern) || subject.match(pattern); if (match) { amount = parseFloat(match[1].replace(/,/g, '')); if (amount > 0) break; } } // Skip if no amount found if (amount <= 0) return null; // Extract merchant const merchantPatterns = [ /from\s+([A-Z][A-Za-z0-9\s&]+?)(?:\s[-—]|\s*<|\n|$)/i, /(?:at|from)\s+([A-Z][A-Za-z0-9\s&]+?)(?:\s[-—]|\s*<|\n|$)/, /([A-Z][A-Za-z0-9\s&]+?)\s*(?:receipt|invoice|order)/i, ]; let merchant = 'Unknown Merchant'; for (const pattern of merchantPatterns) { const match = subject.match(pattern) || from.match(pattern); if (match) { merchant = match[1].trim(); break; } } // Use from email domain as fallback if (merchant === 'Unknown Merchant' && from.includes('@')) { const domain = from.split('@')[1]?.split('>')[0]; if (domain) merchant = domain.replace(/\.com$|\.org$|\.net$/, ''); } // Parse date let date = new Date().toISOString().split('T')[0]; try { const parsed = new Date(dateHeader || Date.now()); if (!isNaN(parsed.getTime())) { date = parsed.toISOString().split('T')[0]; } } catch { // use default } // Categorize based on keywords const category = categorizeReceipt(subject + ' ' + body + ' ' + merchant); // Extract payment method const paymentPatterns = [ /(visa|mastercard|amex|american express|discover|paypal|apple pay|google pay)/i, /ending in (\d{4})/i, /\*\*\*\*(\d{4})/, ]; let paymentMethod = 'Unknown'; for (const pattern of paymentPatterns) { const match = body.match(pattern); if (match) { paymentMethod = match[1] || match[0]; break; } } return { id: message.id, merchant: merchant.substring(0, 100), amount: Math.round(amount * 100) / 100, currency: 'USD', date, category, paymentMethod: paymentMethod.substring(0, 50), emailSubject: subject.substring(0, 200), source: 'gmail', }; } function categorizeReceipt(text: string): string { const lower = text.toLowerCase(); const categories: [string, string[]][] = [ ['Office Supplies', ['office', 'stationery', 'paper', 'ink', 'toner', 'staples', 'office depot']], ['Software & Tools', ['software', 'saas', 'subscription', 'license', 'app', 'tool', 'notion', 'slack', 'github']], ['Travel & Transport', ['flight', 'hotel', 'airbnb', 'uber', 'lyft', 'taxi', 'rental car', 'airline', 'booking']], ['Meals & Entertainment', ['restaurant', 'food', 'meal', 'coffee', 'starbucks', 'doordash', 'grubhub', 'uber eats']], ['Equipment & Hardware', ['computer', 'laptop', 'monitor', 'phone', 'camera', 'electronics', 'best buy', 'amazon']], ['Professional Services', ['legal', 'accounting', 'consulting', 'freelancer', 'upwork', 'fiverr', 'lawyer']], ['Marketing & Advertising', ['ad', 'advertising', 'marketing', 'facebook', 'google ads', 'campaign', 'promotion']], ['Insurance', ['insurance', 'policy', 'premium', 'coverage']], ['Utilities & Internet', ['utility', 'internet', 'phone bill', 'electric', 'gas', 'water']], ['Education & Training', ['course', 'training', 'certification', 'book', 'learning', 'udemy', 'coursera']], ['Health & Medical', ['medical', 'health', 'pharmacy', 'doctor', 'dental', 'vision']], ]; for (const [cat, keywords] of categories) { if (keywords.some(k => lower.includes(k))) return cat; } return 'Other'; } /** * Export receipts to CSV format */ export function exportToCSV(receipts: Receipt[]): string { if (receipts.length === 0) return ''; const headers = ['Date', 'Merchant', 'Category', 'Amount', 'Currency', 'Payment Method', 'Email Subject']; const rows = receipts.map(r => [ r.date, r.merchant, r.category, r.amount.toFixed(2), r.currency, r.paymentMethod, r.emailSubject, ]); const escape = (s: string) => { if (s.includes(',') || s.includes('"') || s.includes('\n')) { return `"${s.replace(/"/g, '""')}"`; } return s; }; return [headers.join(','), ...rows.map(row => row.map(escape).join(','))].join('\n'); } /** * Generate tax summary grouped by category */ export function generateTaxSummary(receipts: Receipt[]): Record { const summary: Record = {}; for (const receipt of receipts) { summary[receipt.category] = (summary[receipt.category] || 0) + receipt.amount; } return summary; }