Init commit!
This commit is contained in:
commit
086d682592
34 changed files with 19249 additions and 0 deletions
42
server/api/balances/index.get.js
Normal file
42
server/api/balances/index.get.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
43
server/api/balances/index.put.js
Normal file
43
server/api/balances/index.put.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
69
server/api/migrate/localStorage.post.js
Normal file
69
server/api/migrate/localStorage.post.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
29
server/api/transactions/[id].delete.js
Normal file
29
server/api/transactions/[id].delete.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
40
server/api/transactions/[id].put.js
Normal file
40
server/api/transactions/[id].put.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
42
server/api/transactions/bulk.put.js
Normal file
42
server/api/transactions/bulk.put.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
23
server/api/transactions/index.get.js
Normal file
23
server/api/transactions/index.get.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
30
server/api/transactions/index.post.js
Normal file
30
server/api/transactions/index.post.js
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
70
server/api/wise/[profile]/balances.get.js
Normal file
70
server/api/wise/[profile]/balances.get.js
Normal 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}`
|
||||
})
|
||||
}
|
||||
})
|
||||
80
server/api/wise/exchange-rates.get.js
Normal file
80
server/api/wise/exchange-rates.get.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue