Initial build: SimpleScan Solo v0.1.0 - Freelancer receipt capture Chrome extension
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
// Test categories
|
||||
import { TAX_CATEGORIES, getCategoryLabel, getCategoryIcon } from '../utils/categories.js';
|
||||
describe('categories', () => {
|
||||
it('has 10 tax categories', () => {
|
||||
assert.strictEqual(TAX_CATEGORIES.length, 10);
|
||||
});
|
||||
it('gets correct label', () => {
|
||||
assert.strictEqual(getCategoryLabel('software'), 'Software');
|
||||
assert.strictEqual(getCategoryLabel('equipment'), 'Equipment');
|
||||
assert.strictEqual(getCategoryLabel('nonexistent'), 'Other');
|
||||
});
|
||||
it('gets correct icon', () => {
|
||||
assert.strictEqual(getCategoryIcon('software'), '⚙️');
|
||||
assert.strictEqual(getCategoryIcon('travel'), '✈️');
|
||||
assert.strictEqual(getCategoryIcon('nonexistent'), '📦');
|
||||
});
|
||||
});
|
||||
// Test export
|
||||
import { exportToCsv } from '../utils/export.js';
|
||||
describe('export', () => {
|
||||
it('exports receipts to CSV', () => {
|
||||
const receipts = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Adobe CC',
|
||||
amount: 59.99,
|
||||
currency: 'USD',
|
||||
category: 'software',
|
||||
date: '2026-06-01',
|
||||
vendor: 'Adobe',
|
||||
notes: 'Monthly',
|
||||
source: 'manual',
|
||||
createdAt: '2026-06-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
const csv = exportToCsv(receipts);
|
||||
console.log('CSV output:', csv);
|
||||
assert.ok(csv.includes('Date'), 'CSV should have Date header');
|
||||
assert.ok(csv.includes('Adobe CC'), 'CSV should have title');
|
||||
assert.ok(csv.includes('59.99'), 'CSV should have amount');
|
||||
assert.ok(csv.includes('Software'), 'CSV should have category label');
|
||||
});
|
||||
it('escapes quotes in CSV', () => {
|
||||
const receipts = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'My "Special" Item',
|
||||
amount: 10,
|
||||
currency: 'USD',
|
||||
category: 'other',
|
||||
date: '2026-06-01',
|
||||
vendor: 'Vendor',
|
||||
notes: '',
|
||||
source: 'manual',
|
||||
createdAt: '2026-06-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
const csv = exportToCsv(receipts);
|
||||
assert.ok(csv.includes('""'), 'CSV should escape quotes');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export const DEFAULT_SETTINGS = {
|
||||
defaultCurrency: 'USD',
|
||||
taxYear: new Date().getFullYear().toString(),
|
||||
ownerName: '',
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
export const TAX_CATEGORIES = [
|
||||
{
|
||||
id: 'equipment',
|
||||
label: 'Equipment',
|
||||
description: 'Computers, cameras, tools, hardware',
|
||||
icon: '💻',
|
||||
},
|
||||
{
|
||||
id: 'home_office',
|
||||
label: 'Home Office',
|
||||
description: 'Desk, chair, lighting, office supplies',
|
||||
icon: '🏠',
|
||||
},
|
||||
{
|
||||
id: 'software',
|
||||
label: 'Software',
|
||||
description: 'Apps, subscriptions, SaaS tools',
|
||||
icon: '⚙️',
|
||||
},
|
||||
{
|
||||
id: 'internet_phone',
|
||||
label: 'Internet & Phone',
|
||||
description: 'ISP, cell plan, domain hosting',
|
||||
icon: '📡',
|
||||
},
|
||||
{
|
||||
id: 'travel',
|
||||
label: 'Travel',
|
||||
description: 'Flights, hotels, rideshare, mileage',
|
||||
icon: '✈️',
|
||||
},
|
||||
{
|
||||
id: 'meals',
|
||||
label: 'Meals',
|
||||
description: 'Business meals (50% deductible)',
|
||||
icon: '🍽️',
|
||||
},
|
||||
{
|
||||
id: 'professional_services',
|
||||
label: 'Professional Services',
|
||||
description: 'Lawyer, accountant, contractor payments',
|
||||
icon: '📋',
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
label: 'Marketing',
|
||||
description: 'Ads, design, copywriting, PR',
|
||||
icon: '📢',
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
label: 'Education',
|
||||
description: 'Courses, books, conferences',
|
||||
icon: '📚',
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
label: 'Other',
|
||||
description: 'Miscellaneous business expenses',
|
||||
icon: '📦',
|
||||
},
|
||||
];
|
||||
export function getCategoryLabel(id) {
|
||||
return TAX_CATEGORIES.find((c) => c.id === id)?.label || 'Other';
|
||||
}
|
||||
export function getCategoryIcon(id) {
|
||||
return TAX_CATEGORIES.find((c) => c.id === id)?.icon || '📦';
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getCategoryLabel } from './categories.js';
|
||||
export function exportToCsv(receipts) {
|
||||
const headers = ['Date', 'Vendor', 'Title', 'Category', 'Amount', 'Currency', 'Notes', 'Source'];
|
||||
const rows = receipts.map((r) => [
|
||||
r.date,
|
||||
r.vendor,
|
||||
r.title,
|
||||
getCategoryLabel(r.category),
|
||||
r.amount.toFixed(2),
|
||||
r.currency,
|
||||
r.notes,
|
||||
r.source,
|
||||
]);
|
||||
const csv = [headers, ...rows]
|
||||
.map((row) => row
|
||||
.map((cell) => {
|
||||
const str = String(cell).replace(/"/g, '""');
|
||||
return `"${str}"`;
|
||||
})
|
||||
.join(','))
|
||||
.join('\n');
|
||||
return csv;
|
||||
}
|
||||
export function downloadBlob(content, filename, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DEFAULT_SETTINGS } from '../types.js';
|
||||
const RECEIPTS_KEY = 'simplescan_receipts';
|
||||
const SETTINGS_KEY = 'simplescan_settings';
|
||||
export async function getReceipts() {
|
||||
const result = await chrome.storage.local.get(RECEIPTS_KEY);
|
||||
return result[RECEIPTS_KEY] || [];
|
||||
}
|
||||
export async function saveReceipt(receipt) {
|
||||
const receipts = await getReceipts();
|
||||
const existingIndex = receipts.findIndex((r) => r.id === receipt.id);
|
||||
if (existingIndex >= 0) {
|
||||
receipts[existingIndex] = receipt;
|
||||
}
|
||||
else {
|
||||
receipts.unshift(receipt);
|
||||
}
|
||||
await chrome.storage.local.set({ [RECEIPTS_KEY]: receipts });
|
||||
}
|
||||
export async function deleteReceipt(id) {
|
||||
const receipts = await getReceipts();
|
||||
const filtered = receipts.filter((r) => r.id !== id);
|
||||
await chrome.storage.local.set({ [RECEIPTS_KEY]: filtered });
|
||||
}
|
||||
export async function getSettings() {
|
||||
const result = await chrome.storage.local.get(SETTINGS_KEY);
|
||||
return result[SETTINGS_KEY] || { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
export async function saveSettings(settings) {
|
||||
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
|
||||
}
|
||||
export async function clearAllReceipts() {
|
||||
await chrome.storage.local.remove(RECEIPTS_KEY);
|
||||
}
|
||||
Reference in New Issue
Block a user