Init commit!

This commit is contained in:
Jennie Robinson Faber 2025-08-22 18:36:16 +01:00
commit 086d682592
34 changed files with 19249 additions and 0 deletions

View file

@ -0,0 +1,42 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const collection = await getCollection('balances')
const balances = await collection.findOne({ type: 'current' })
if (!balances) {
// Return default balance structure if none exists
return {
currentBalance: 0,
accountBalances: {
manual: {
rbc_cad: 0,
td_cad: 0,
millennium_eur: 0
},
wise: {
jennie: [],
henry: []
},
lastWiseFetch: null,
lastManualUpdate: null
}
}
}
// Ensure timestamp fields exist
if (balances.accountBalances && !balances.accountBalances.lastWiseFetch && !balances.accountBalances.lastManualUpdate) {
balances.accountBalances.lastWiseFetch = null;
balances.accountBalances.lastManualUpdate = null;
}
return balances
} catch (error) {
console.error('Error fetching balances:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch balances'
})
}
})

View file

@ -0,0 +1,43 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const collection = await getCollection('balances')
// Get existing balance document to preserve timestamp data
const existingBalance = await collection.findOne({ type: 'current' })
const balanceData = {
type: 'current',
currentBalance: body.currentBalance,
accountBalances: {
manual: body.accountBalances?.manual || {},
wise: body.accountBalances?.wise || { jennie: [], henry: [] },
// Preserve existing timestamps if not explicitly provided
lastWiseFetch: body.accountBalances?.lastWiseFetch || existingBalance?.accountBalances?.lastWiseFetch || null,
lastManualUpdate: body.accountBalances?.lastManualUpdate || existingBalance?.accountBalances?.lastManualUpdate || null
},
updatedAt: new Date()
}
// Upsert the balance record
const result = await collection.replaceOne(
{ type: 'current' },
balanceData,
{ upsert: true }
)
return {
success: true,
modifiedCount: result.modifiedCount,
upsertedCount: result.upsertedCount
}
} catch (error) {
console.error('Error updating balances:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to update balances'
})
}
})

View file

@ -0,0 +1,69 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const data = await readBody(event)
// Migrate transactions
if (data.transactions && data.transactions.length > 0) {
const transactionsCollection = await getCollection('transactions')
// Clear existing transactions
await transactionsCollection.deleteMany({})
// Convert and insert transactions
const mongoTransactions = data.transactions.map(transaction => ({
...transaction,
_id: transaction.id,
id: undefined,
date: new Date(transaction.date),
endDate: transaction.endDate ? new Date(transaction.endDate) : null,
createdAt: new Date(),
updatedAt: new Date()
}))
await transactionsCollection.insertMany(mongoTransactions)
}
// Migrate balance
if (data.balance !== undefined) {
const balancesCollection = await getCollection('balances')
const balanceData = {
type: 'current',
currentBalance: data.balance,
accountBalances: data.accountBalances || {
manual: {
rbc_cad: 0,
td_cad: 0,
millennium_eur: 0
},
wise: {
jennie: [],
henry: []
}
},
updatedAt: new Date()
}
await balancesCollection.replaceOne(
{ type: 'current' },
balanceData,
{ upsert: true }
)
}
return {
success: true,
message: 'Data migrated successfully',
transactionsCount: data.transactions?.length || 0,
balance: data.balance || 0
}
} catch (error) {
console.error('Error migrating localStorage data:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to migrate data'
})
}
})

View file

@ -0,0 +1,29 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const id = getRouterParam(event, 'id')
const collection = await getCollection('transactions')
// Delete the transaction
const result = await collection.deleteOne({ _id: id })
if (result.deletedCount === 0) {
throw createError({
statusCode: 404,
statusMessage: 'Transaction not found'
})
}
return {
success: true,
deletedCount: result.deletedCount
}
} catch (error) {
console.error('Error deleting transaction:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to delete transaction'
})
}
})

View file

@ -0,0 +1,40 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const collection = await getCollection('transactions')
// Update document
const updateData = {
...body,
_id: id,
id: undefined,
updatedAt: new Date()
}
const result = await collection.replaceOne(
{ _id: id },
updateData
)
if (result.matchedCount === 0) {
throw createError({
statusCode: 404,
statusMessage: 'Transaction not found'
})
}
return {
success: true,
modifiedCount: result.modifiedCount
}
} catch (error) {
console.error('Error updating transaction:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to update transaction'
})
}
})

View file

@ -0,0 +1,42 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const transactions = await readBody(event)
const collection = await getCollection('transactions')
// Clear existing transactions and insert new ones
await collection.deleteMany({})
if (transactions.length > 0) {
// Convert transactions for MongoDB
const mongoTransactions = transactions.map(transaction => ({
...transaction,
_id: transaction.id,
id: undefined,
date: new Date(transaction.date),
endDate: transaction.endDate ? new Date(transaction.endDate) : null,
createdAt: new Date(),
updatedAt: new Date()
}))
const result = await collection.insertMany(mongoTransactions)
return {
success: true,
insertedCount: result.insertedCount
}
}
return {
success: true,
insertedCount: 0
}
} catch (error) {
console.error('Error bulk updating transactions:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to bulk update transactions'
})
}
})

View file

@ -0,0 +1,23 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const collection = await getCollection('transactions')
const transactions = await collection.find({}).toArray()
// Convert MongoDB _id to id for frontend compatibility
const formattedTransactions = transactions.map(transaction => ({
...transaction,
id: transaction._id.toString(),
_id: undefined
}))
return formattedTransactions
} catch (error) {
console.error('Error fetching transactions:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch transactions'
})
}
})

View file

@ -0,0 +1,30 @@
import { getCollection } from '../../utils/db.js'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const collection = await getCollection('transactions')
// Convert id to _id for MongoDB
const transactionData = {
...body,
_id: body.id,
id: undefined,
createdAt: new Date(),
updatedAt: new Date()
}
const result = await collection.insertOne(transactionData)
return {
success: true,
id: result.insertedId.toString()
}
} catch (error) {
console.error('Error creating transaction:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to create transaction'
})
}
})

View file

@ -0,0 +1,70 @@
export default defineEventHandler(async (event) => {
const profile = getRouterParam(event, 'profile') // 'jennie' or 'henry'
// Get API token from environment variables
const apiToken = profile === 'jennie'
? process.env.WISE_API_KEY_JENNIE
: process.env.WISE_API_KEY_HENRY
if (!apiToken) {
throw createError({
statusCode: 400,
statusMessage: `Wise API token not configured for ${profile}`
})
}
try {
// First, get the profile ID
const profileResponse = await fetch('https://api.wise.com/v1/profiles', {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
})
if (!profileResponse.ok) {
throw new Error(`Profile fetch failed: ${profileResponse.statusText}`)
}
const profiles = await profileResponse.json()
const personalProfile = profiles.find(p => p.type === 'personal')
if (!personalProfile) {
throw new Error('Personal profile not found')
}
// Get balances for the profile
const balancesResponse = await fetch(`https://api.wise.com/v4/profiles/${personalProfile.id}/balances?types=STANDARD`, {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
})
if (!balancesResponse.ok) {
throw new Error(`Balances fetch failed: ${balancesResponse.statusText}`)
}
const balances = await balancesResponse.json()
// Filter and format the balances - include all currencies
const formattedBalances = balances
.filter(balance => balance.amount) // Only filter out balances without amount data
.map(balance => ({
currency: balance.amount.currency,
value: {
value: balance.amount.value
}
}))
return formattedBalances
} catch (error) {
console.error(`Wise API error for ${profile}:`, error)
throw createError({
statusCode: 500,
statusMessage: `Failed to fetch Wise balances: ${error.message}`
})
}
})

View file

@ -0,0 +1,80 @@
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const { source = 'EUR', target = 'CAD' } = query
// Get API token from environment variables (use Jennie's token by default)
const apiToken = process.env.WISE_API_KEY_JENNIE
if (!apiToken) {
throw createError({
statusCode: 400,
statusMessage: 'Wise API token not configured'
})
}
try {
// First get the profile ID
const profileResponse = await fetch('https://api.wise.com/v1/profiles', {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
})
if (!profileResponse.ok) {
throw new Error(`Profile fetch failed: ${profileResponse.statusText}`)
}
const profiles = await profileResponse.json()
const personalProfile = profiles.find(p => p.type === 'personal')
if (!personalProfile) {
throw new Error('Personal profile not found')
}
// Get exchange rates
const ratesResponse = await fetch(`https://api.wise.com/v1/rates?source=${source}&target=${target}`, {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
})
if (!ratesResponse.ok) {
throw new Error(`Exchange rates fetch failed: ${ratesResponse.statusText}`)
}
const rates = await ratesResponse.json()
// Return the rate and timestamp
return {
source,
target,
rate: rates[0]?.rate || null,
timestamp: new Date().toISOString(),
rateType: rates[0]?.type || 'unknown'
}
} catch (error) {
console.error(`Wise exchange rate API error:`, error)
// Fallback to hardcoded rate if API fails
const fallbackRates = {
'EUR-CAD': 1.45,
'USD-CAD': 1.35,
'GBP-CAD': 1.65
}
const fallbackKey = `${source}-${target}`
const fallbackRate = fallbackRates[fallbackKey] || 1.0
return {
source,
target,
rate: fallbackRate,
timestamp: new Date().toISOString(),
rateType: 'fallback',
error: 'Using fallback rate due to API error'
}
}
})

41
server/utils/db.js Normal file
View file

@ -0,0 +1,41 @@
import { MongoClient } from 'mongodb'
let client = null
let db = null
export async function connectToDatabase() {
if (db) {
return db
}
const uri = process.env.MONGO_URI
if (!uri) {
throw new Error('MONGO_URI environment variable is not set')
}
try {
client = new MongoClient(uri)
await client.connect()
db = client.db('faber-finances')
console.log('Connected to MongoDB')
return db
} catch (error) {
console.error('Failed to connect to MongoDB:', error)
throw error
}
}
export async function getCollection(name) {
const database = await connectToDatabase()
return database.collection(name)
}
// Close connection (useful for cleanup)
export async function closeDatabaseConnection() {
if (client) {
await client.close()
client = null
db = null
console.log('Disconnected from MongoDB')
}
}