Files
homepage/src/routes/api/cospend/balance/+server.ts
Alexander Bocken baa3f3e533
All checks were successful
CI / update (push) Successful in 1m21s
feat: add Redis caching to cospend API endpoints
Implement comprehensive caching for all cospend routes to improve performance:

Cache Implementation:
- Balance API: 30-minute TTL for user balances and global balances
- Debts API: 15-minute TTL for debt breakdown calculations
- Payments List: 10-minute TTL with pagination support
- Individual Payment: 30-minute TTL for payment details

Cache Invalidation:
- Created invalidateCospendCaches() helper function
- Invalidates user balances, debts, and payment lists on mutations
- Applied to payment create, update, and delete operations
- Applied to recurring payment execution (manual and cron)
2026-01-13 19:45:11 +01:00

125 lines
3.5 KiB
TypeScript

import type { RequestHandler } from '@sveltejs/kit';
import { PaymentSplit } from '../../../../models/PaymentSplit';
import { Payment } from '../../../../models/Payment'; // Need to import Payment for populate to work
import { dbConnect } from '../../../../utils/db';
import { error, json } from '@sveltejs/kit';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({ locals, url }) => {
const auth = await locals.auth();
if (!auth || !auth.user?.nickname) {
throw error(401, 'Not logged in');
}
const username = auth.user.nickname;
const includeAll = url.searchParams.get('all') === 'true';
await dbConnect();
try {
if (includeAll) {
// Try cache first for all balances
const cacheKey = 'cospend:balance:all';
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const allSplits = await PaymentSplit.aggregate([
{
$group: {
_id: '$username',
totalOwed: { $sum: { $cond: [{ $gt: ['$amount', 0] }, '$amount', 0] } },
totalOwing: { $sum: { $cond: [{ $lt: ['$amount', 0] }, { $abs: '$amount' }, 0] } },
netBalance: { $sum: '$amount' }
}
},
{
$project: {
username: '$_id',
totalOwed: 1,
totalOwing: 1,
netBalance: 1,
_id: 0
}
}
]);
const currentUserBalance = allSplits.find(balance => balance.username === username) || {
username,
totalOwed: 0,
totalOwing: 0,
netBalance: 0
};
const result = {
currentUser: currentUserBalance,
allBalances: allSplits
};
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
} else {
// Try cache first for individual user balance
const cacheKey = `cospend:balance:${username}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const userSplits = await PaymentSplit.find({ username }).lean();
// Calculate net balance: negative = you are owed money, positive = you owe money
const netBalance = userSplits.reduce((sum, split) => sum + split.amount, 0);
const recentSplits = await PaymentSplit.aggregate([
{ $match: { username } },
{
$lookup: {
from: 'payments',
localField: 'paymentId',
foreignField: '_id',
as: 'paymentId'
}
},
{ $unwind: '$paymentId' },
{ $sort: { 'paymentId.date': -1, 'paymentId.createdAt': -1 } },
{ $limit: 10 }
]);
// For settlements, fetch the other user's split info
for (const split of recentSplits) {
if (split.paymentId && split.paymentId.category === 'settlement') {
// This is a settlement, find the other user
const otherSplit = await PaymentSplit.findOne({
paymentId: split.paymentId._id,
username: { $ne: username }
}).lean();
if (otherSplit) {
split.otherUser = otherSplit.username;
}
}
}
const result = {
netBalance,
recentSplits
};
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
}
} catch (e) {
console.error('Error calculating balance:', e);
throw error(500, 'Failed to calculate balance');
}
};