diff --git a/src/lib/server/cache.ts b/src/lib/server/cache.ts index 5af2b83..dd9b4fb 100644 --- a/src/lib/server/cache.ts +++ b/src/lib/server/cache.ts @@ -302,10 +302,44 @@ export async function invalidateRecipeCaches(): Promise { cache.delPattern('recipes:category:*'), cache.delPattern('recipes:icon:*'), ]); - console.log('[Cache] Invalidated all recipe caches'); } catch (err) { console.error('[Cache] Error invalidating recipe caches:', err); } } +/** + * Helper function to invalidate cospend caches for specific users and/or payments + * Call this after payment create/update/delete operations + * @param usernames - Array of usernames whose caches should be invalidated + * @param paymentId - Optional payment ID to invalidate specific payment cache + */ +export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise { + try { + const invalidations: Promise[] = []; + + // Invalidate balance and debts caches for all affected users + for (const username of usernames) { + invalidations.push( + cache.del(`cospend:balance:${username}`), + cache.del(`cospend:debts:${username}`) + ); + } + + // Invalidate global balance cache + invalidations.push(cache.del('cospend:balance:all')); + + // Invalidate payment list caches (all pagination variants) + invalidations.push(cache.delPattern('cospend:payments:list:*')); + + // If specific payment ID provided, invalidate its cache + if (paymentId) { + invalidations.push(cache.del(`cospend:payment:${paymentId}`)); + } + + await Promise.all(invalidations); + } catch (err) { + console.error('[Cache] Error invalidating cospend caches:', err); + } +} + export default cache; diff --git a/src/routes/api/cospend/balance/+server.ts b/src/routes/api/cospend/balance/+server.ts index ab2c4b2..39f5880 100644 --- a/src/routes/api/cospend/balance/+server.ts +++ b/src/routes/api/cospend/balance/+server.ts @@ -3,6 +3,7 @@ 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(); @@ -14,9 +15,17 @@ export const GET: RequestHandler = async ({ locals, url }) => { 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: { @@ -44,14 +53,27 @@ export const GET: RequestHandler = async ({ locals, url }) => { netBalance: 0 }; - return json({ + 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); @@ -78,17 +100,22 @@ export const GET: RequestHandler = async ({ locals, url }) => { paymentId: split.paymentId._id, username: { $ne: username } }).lean(); - + if (otherSplit) { split.otherUser = otherSplit.username; } } } - return json({ + const result = { netBalance, recentSplits - }); + }; + + // Cache for 30 minutes + await cache.set(cacheKey, JSON.stringify(result), 1800); + + return json(result); } } catch (e) { diff --git a/src/routes/api/cospend/debts/+server.ts b/src/routes/api/cospend/debts/+server.ts index f642475..67d58b4 100644 --- a/src/routes/api/cospend/debts/+server.ts +++ b/src/routes/api/cospend/debts/+server.ts @@ -3,6 +3,7 @@ import { PaymentSplit } from '../../../../models/PaymentSplit'; import { Payment } from '../../../../models/Payment'; import { dbConnect } from '../../../../utils/db'; import { error, json } from '@sveltejs/kit'; +import cache from '$lib/server/cache'; interface DebtSummary { username: string; @@ -25,8 +26,16 @@ export const GET: RequestHandler = async ({ locals }) => { const currentUser = auth.user.nickname; await dbConnect(); - + try { + // Try cache first + const cacheKey = `cospend:debts:${currentUser}`; + const cached = await cache.get(cacheKey); + + if (cached) { + return json(JSON.parse(cached)); + } + // Get all splits for the current user const userSplits = await PaymentSplit.find({ username: currentUser }) .populate('paymentId') @@ -50,13 +59,13 @@ export const GET: RequestHandler = async ({ locals }) => { if (!payment) continue; // Find other participants in this payment - const otherSplits = allRelatedSplits.filter(s => + const otherSplits = allRelatedSplits.filter(s => s.paymentId._id.toString() === split.paymentId._id.toString() ); for (const otherSplit of otherSplits) { const otherUser = otherSplit.username; - + if (!debtsByUser.has(otherUser)) { debtsByUser.set(otherUser, { username: otherUser, @@ -66,11 +75,11 @@ export const GET: RequestHandler = async ({ locals }) => { } const debt = debtsByUser.get(otherUser)!; - + // Current user's amount: positive = they owe, negative = they are owed // We want to show net between the two users debt.netAmount += split.amount; - + debt.transactions.push({ paymentId: payment._id.toString(), title: payment.title, @@ -97,12 +106,17 @@ export const GET: RequestHandler = async ({ locals }) => { netAmount: Math.round(debt.netAmount * 100) / 100 // Round to 2 decimal places })); - return json({ + const result = { whoOwesMe, whoIOwe, totalOwedToMe: whoOwesMe.reduce((sum, debt) => sum + debt.netAmount, 0), totalIOwe: whoIOwe.reduce((sum, debt) => sum + debt.netAmount, 0) - }); + }; + + // Cache for 15 minutes (as suggested in plan for debt breakdown) + await cache.set(cacheKey, JSON.stringify(result), 900); + + return json(result); } catch (e) { console.error('Error calculating debt breakdown:', e); diff --git a/src/routes/api/cospend/payments/+server.ts b/src/routes/api/cospend/payments/+server.ts index bc397e1..ff8a6c9 100644 --- a/src/routes/api/cospend/payments/+server.ts +++ b/src/routes/api/cospend/payments/+server.ts @@ -4,6 +4,7 @@ import { PaymentSplit } from '../../../../models/PaymentSplit'; import { dbConnect } from '../../../../utils/db'; import { convertToCHF, isValidCurrencyCode } from '../../../../lib/utils/currency'; import { error, json } from '@sveltejs/kit'; +import cache, { invalidateCospendCaches } from '$lib/server/cache'; export const GET: RequestHandler = async ({ locals, url }) => { const auth = await locals.auth(); @@ -15,8 +16,16 @@ export const GET: RequestHandler = async ({ locals, url }) => { const offset = parseInt(url.searchParams.get('offset') || '0'); await dbConnect(); - + try { + // Try cache first (include pagination params in key) + const cacheKey = `cospend:payments:list:${limit}:${offset}`; + const cached = await cache.get(cacheKey); + + if (cached) { + return json(JSON.parse(cached)); + } + const payments = await Payment.find() .populate('splits') .sort({ date: -1, createdAt: -1 }) @@ -24,7 +33,12 @@ export const GET: RequestHandler = async ({ locals, url }) => { .skip(offset) .lean(); - return json({ payments }); + const result = { payments }; + + // Cache for 10 minutes (shorter TTL since this changes frequently) + await cache.set(cacheKey, JSON.stringify(result), 600); + + return json(result); } catch (e) { throw error(500, 'Failed to fetch payments'); } finally { @@ -138,9 +152,13 @@ export const POST: RequestHandler = async ({ request, locals }) => { await Promise.all(splitPromises); - return json({ - success: true, - payment: payment._id + // Invalidate caches for all affected users + const affectedUsernames = splits.map((split: any) => split.username); + await invalidateCospendCaches(affectedUsernames, payment._id.toString()); + + return json({ + success: true, + payment: payment._id }); } catch (e) { diff --git a/src/routes/api/cospend/payments/[id]/+server.ts b/src/routes/api/cospend/payments/[id]/+server.ts index 5296a3b..c1905b4 100644 --- a/src/routes/api/cospend/payments/[id]/+server.ts +++ b/src/routes/api/cospend/payments/[id]/+server.ts @@ -3,6 +3,7 @@ import { Payment } from '../../../../../models/Payment'; import { PaymentSplit } from '../../../../../models/PaymentSplit'; import { dbConnect } from '../../../../../utils/db'; import { error, json } from '@sveltejs/kit'; +import cache, { invalidateCospendCaches } from '$lib/server/cache'; export const GET: RequestHandler = async ({ params, locals }) => { const auth = await locals.auth(); @@ -13,15 +14,28 @@ export const GET: RequestHandler = async ({ params, locals }) => { const { id } = params; await dbConnect(); - + try { + // Try cache first + const cacheKey = `cospend:payment:${id}`; + const cached = await cache.get(cacheKey); + + if (cached) { + return json(JSON.parse(cached)); + } + const payment = await Payment.findById(id).populate('splits').lean(); - + if (!payment) { throw error(404, 'Payment not found'); } - return json({ payment }); + const result = { payment }; + + // Cache for 30 minutes + await cache.set(cacheKey, JSON.stringify(result), 1800); + + return json(result); } catch (e) { if (e.status === 404) throw e; throw error(500, 'Failed to fetch payment'); @@ -40,10 +54,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { const data = await request.json(); await dbConnect(); - + try { const payment = await Payment.findById(id); - + if (!payment) { throw error(404, 'Payment not found'); } @@ -52,9 +66,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { throw error(403, 'Not authorized to edit this payment'); } + // Get old splits to invalidate caches for users who were in the original payment + const oldSplits = await PaymentSplit.find({ paymentId: id }).lean(); + const oldUsernames = oldSplits.map(split => split.username); + const updatedPayment = await Payment.findByIdAndUpdate( id, - { + { title: data.title, description: data.description, amount: data.amount, @@ -67,9 +85,10 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { { new: true } ); + let newUsernames: string[] = []; if (data.splits) { await PaymentSplit.deleteMany({ paymentId: id }); - + const splitPromises = data.splits.map((split: any) => { return PaymentSplit.create({ paymentId: id, @@ -81,8 +100,13 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { }); await Promise.all(splitPromises); + newUsernames = data.splits.map((split: any) => split.username); } + // Invalidate caches for all users (old and new) + const allAffectedUsers = [...new Set([...oldUsernames, ...newUsernames])]; + await invalidateCospendCaches(allAffectedUsers, id); + return json({ success: true, payment: updatedPayment }); } catch (e) { if (e.status) throw e; @@ -101,10 +125,10 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { const { id } = params; await dbConnect(); - + try { const payment = await Payment.findById(id); - + if (!payment) { throw error(404, 'Payment not found'); } @@ -113,9 +137,16 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { throw error(403, 'Not authorized to delete this payment'); } + // Get splits to invalidate caches for affected users + const splits = await PaymentSplit.find({ paymentId: id }).lean(); + const affectedUsernames = splits.map(split => split.username); + await PaymentSplit.deleteMany({ paymentId: id }); await Payment.findByIdAndDelete(id); + // Invalidate caches for all affected users + await invalidateCospendCaches(affectedUsernames, id); + return json({ success: true }); } catch (e) { if (e.status) throw e; diff --git a/src/routes/api/cospend/recurring-payments/cron-execute/+server.ts b/src/routes/api/cospend/recurring-payments/cron-execute/+server.ts index 09e0a17..6a88fff 100644 --- a/src/routes/api/cospend/recurring-payments/cron-execute/+server.ts +++ b/src/routes/api/cospend/recurring-payments/cron-execute/+server.ts @@ -5,6 +5,7 @@ import { PaymentSplit } from '../../../../../models/PaymentSplit'; import { dbConnect } from '../../../../../utils/db'; import { error, json } from '@sveltejs/kit'; import { calculateNextExecutionDate } from '../../../../../lib/utils/recurring'; +import { invalidateCospendCaches } from '$lib/server/cache'; // This endpoint is designed to be called by a cron job or external scheduler // It processes all recurring payments that are due for execution @@ -65,6 +66,10 @@ export const POST: RequestHandler = async ({ request }) => { await Promise.all(splitPromises); + // Invalidate caches for all affected users + const affectedUsernames = recurringPayment.splits.map((split) => split.username); + await invalidateCospendCaches(affectedUsernames, payment._id.toString()); + // Calculate next execution date const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now); diff --git a/src/routes/api/cospend/recurring-payments/execute/+server.ts b/src/routes/api/cospend/recurring-payments/execute/+server.ts index 34ef135..7163dd7 100644 --- a/src/routes/api/cospend/recurring-payments/execute/+server.ts +++ b/src/routes/api/cospend/recurring-payments/execute/+server.ts @@ -6,6 +6,7 @@ import { dbConnect } from '../../../../../utils/db'; import { error, json } from '@sveltejs/kit'; import { calculateNextExecutionDate } from '../../../../../lib/utils/recurring'; import { convertToCHF } from '../../../../../lib/utils/currency'; +import { invalidateCospendCaches } from '$lib/server/cache'; export const POST: RequestHandler = async ({ locals }) => { const auth = await locals.auth(); @@ -98,6 +99,10 @@ export const POST: RequestHandler = async ({ locals }) => { await Promise.all(splitPromises); + // Invalidate caches for all affected users + const affectedUsernames = recurringPayment.splits.map((split) => split.username); + await invalidateCospendCaches(affectedUsernames, payment._id.toString()); + // Calculate next execution date const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now);