TaxPack v1.0.0 - Gmail Receipt Auto-Extractor & Tax Report Generator
This commit is contained in:
@@ -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
|
||||
});
|
||||
Reference in New Issue
Block a user