TaxPack v1.0.0 - Gmail Receipt Auto-Extractor & Tax Report Generator

This commit is contained in:
Bun Bun
2026-06-19 06:28:06 +00:00
commit d0a0615c28
17 changed files with 4309 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
// TaxPack Background Service Worker
// Handles Gmail OAuth and message passing
import {
Receipt,
extractReceiptFromEmail,
exportToCSV,
generateTaxSummary,
} from './lib/receipt-utils';
// Store for extracted receipts
const STORAGE_KEY = 'taxpack_receipts';
// Get auth token from Chrome identity API
export async function getAuthToken(interactive: boolean = true): Promise<string | null> {
try {
const token = await chrome.identity.getAuthToken({ interactive });
return token.token ?? null;
} catch (error) {
console.error('Auth error:', error);
return null;
}
}
// Fetch Gmail messages with receipt indicators
export async function fetchReceiptEmails(token: string, maxResults: number = 50): Promise<any[]> {
const queries = [
'subject:receipt',
'subject:invoice',
'subject:order confirmation',
'subject:payment confirmation',
'subject:purchase confirmation',
'from:receipts',
'from:noreply',
];
const allMessages: any[] = [];
const seenIds = new Set<string>();
for (const query of queries) {
try {
const url = `https://www.googleapis.com/gmail/v1/users/me/messages?q=${encodeURIComponent(query)}&maxResults=${Math.ceil(maxResults / queries.length)}`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
if (response.status === 401) {
// Token expired, try to remove and re-auth
await chrome.identity.removeCachedAuthToken({ token });
}
continue;
}
const data = await response.json();
if (data.messages) {
for (const msg of data.messages) {
if (!seenIds.has(msg.id)) {
seenIds.add(msg.id);
allMessages.push(msg);
}
}
}
} catch (error) {
console.error(`Query failed: ${query}`, error);
}
}
return allMessages.slice(0, maxResults);
}
// Fetch full message details
export async function fetchMessageDetails(token: string, messageId: string): Promise<any | null> {
try {
const url = `https://www.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=full`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return null;
return await response.json();
} catch (error) {
console.error('Fetch message error:', error);
return null;
}
}
// Save receipts to storage
export async function saveReceipts(receipts: Receipt[]): Promise<void> {
const existing = await loadReceipts();
const merged = [...existing];
const seen = new Set(existing.map(r => r.id));
for (const receipt of receipts) {
if (!seen.has(receipt.id)) {
seen.add(receipt.id);
merged.push(receipt);
}
}
await chrome.storage.local.set({ [STORAGE_KEY]: merged });
}
// Load receipts from storage
export async function loadReceipts(): Promise<Receipt[]> {
const result = await chrome.storage.local.get(STORAGE_KEY);
return result[STORAGE_KEY] || [];
}
// Clear all receipts
export async function clearReceipts(): Promise<void> {
await chrome.storage.local.remove(STORAGE_KEY);
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
(async () => {
try {
if (request.action === 'scanGmail') {
const token = await getAuthToken(request.interactive ?? true);
if (!token) {
sendResponse({ success: false, error: 'Authentication failed' });
return;
}
const messages = await fetchReceiptEmails(token, request.maxResults || 50);
const receipts: Receipt[] = [];
for (const msg of messages.slice(0, 20)) {
const details = await fetchMessageDetails(token, msg.id);
if (details) {
const receipt = extractReceiptFromEmail(details);
if (receipt) receipts.push(receipt);
}
}
await saveReceipts(receipts);
sendResponse({ success: true, count: receipts.length, total: messages.length });
} else if (request.action === 'getReceipts') {
const receipts = await loadReceipts();
sendResponse({ success: true, receipts });
} else if (request.action === 'clearReceipts') {
await clearReceipts();
sendResponse({ success: true });
} else if (request.action === 'exportCSV') {
const receipts = await loadReceipts();
const csv = exportToCSV(receipts);
sendResponse({ success: true, csv });
} else if (request.action === 'getSummary') {
const receipts = await loadReceipts();
const summary = generateTaxSummary(receipts);
sendResponse({ success: true, summary, total: receipts.reduce((s, r) => s + r.amount, 0) });
} else {
sendResponse({ success: false, error: 'Unknown action' });
}
} catch (error) {
sendResponse({ success: false, error: String(error) });
}
})();
return true; // Keep channel open for async
});