import { users, transactions, cooldowns } from "@/db/schema"; import { eq, sql, and } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; const DAILY_REWARD_AMOUNT = 100n; const STREAK_BONUS = 10n; const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in ms export const economyService = { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => { if (amount <= 0n) { throw new Error("Amount must be positive"); } if (fromUserId === toUserId) { throw new Error("Cannot transfer to self"); } const execute = async (txFn: any) => { // Check sender balance const sender = await txFn.query.users.findFirst({ where: eq(users.id, BigInt(fromUserId)), }); if (!sender) { throw new Error("Sender not found"); } if ((sender.balance ?? 0n) < amount) { throw new Error("Insufficient funds"); } // Deduct from sender await txFn.update(users) .set({ balance: sql`${users.balance} - ${amount}`, }) .where(eq(users.id, BigInt(fromUserId))); // Add to receiver await txFn.update(users) .set({ balance: sql`${users.balance} + ${amount}`, }) .where(eq(users.id, BigInt(toUserId))); // Create transaction records // 1. Debit for sender await txFn.insert(transactions).values({ userId: BigInt(fromUserId), amount: -amount, type: 'TRANSFER_OUT', description: `Transfer to ${toUserId}`, }); // 2. Credit for receiver await txFn.insert(transactions).values({ userId: BigInt(toUserId), amount: amount, type: 'TRANSFER_IN', description: `Transfer from ${fromUserId}`, }); return { success: true, amount }; }; if (tx) { return await execute(tx); } else { return await DrizzleClient.transaction(async (t) => { return await execute(t); }); } }, claimDaily: async (userId: string, tx?: any) => { const execute = async (txFn: any) => { const now = new Date(); const startOfDay = new Date(now); startOfDay.setHours(0, 0, 0, 0); // Check cooldown const cooldown = await txFn.query.cooldowns.findFirst({ where: and( eq(cooldowns.userId, BigInt(userId)), eq(cooldowns.actionKey, 'daily') ), }); if (cooldown && cooldown.readyAt > now) { throw new Error(`Daily already claimed. Ready at ${cooldown.readyAt}`); } // Get user for streak logic const user = await txFn.query.users.findFirst({ where: eq(users.id, BigInt(userId)), }); if (!user) { throw new Error("User not found"); } let streak = (user.dailyStreak || 0) + 1; // If previous cooldown exists and expired more than 24h ago (meaning >48h since last claim), reset streak if (cooldown) { const timeSinceReady = now.getTime() - cooldown.readyAt.getTime(); if (timeSinceReady > 24 * 60 * 60 * 1000) { streak = 1; } } else { streak = 1; } const bonus = (BigInt(streak) - 1n) * STREAK_BONUS; const totalReward = DAILY_REWARD_AMOUNT + bonus; // Update User w/ Economy Service (reuse modifyUserBalance if we split it out, but here manual is fine for atomic combined streak update) // Actually, we can just update directly here as we are already refining specific fields like streak. await txFn.update(users) .set({ balance: sql`${users.balance} + ${totalReward}`, dailyStreak: streak, xp: sql`${users.xp} + 10`, // Small XP reward for daily }) .where(eq(users.id, BigInt(userId))); // Set new cooldown (now + 24h) const nextReadyAt = new Date(now.getTime() + DAILY_COOLDOWN); await txFn.insert(cooldowns) .values({ userId: BigInt(userId), actionKey: 'daily', readyAt: nextReadyAt, }) .onConflictDoUpdate({ target: [cooldowns.userId, cooldowns.actionKey], set: { readyAt: nextReadyAt }, }); // Log Transaction await txFn.insert(transactions).values({ userId: BigInt(userId), amount: totalReward, type: 'DAILY_REWARD', description: `Daily reward (Streak: ${streak})`, }); return { claimed: true, amount: totalReward, streak, nextReadyAt }; }; if (tx) { return await execute(tx); } else { return await DrizzleClient.transaction(async (t) => { return await execute(t); }); } }, modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, tx?: any) => { const execute = async (txFn: any) => { if (amount < 0n) { // Check sufficient funds if removing const user = await txFn.query.users.findFirst({ where: eq(users.id, BigInt(id)) }); if (!user || (user.balance ?? 0n) < -amount) { throw new Error("Insufficient funds"); } } const [user] = await txFn.update(users) .set({ balance: sql`${users.balance} + ${amount}`, }) .where(eq(users.id, BigInt(id))) .returning(); await txFn.insert(transactions).values({ userId: BigInt(id), amount: amount, type: type, description: description, }); return user; }; if (tx) { return await execute(tx); } else { return await DrizzleClient.transaction(async (t) => { return await execute(t); }); } }, };