import { AppDataSource } from '../config/database'; import { Wallet } from '../entities/Wallet'; import { Transaction, TransactionType, TransactionStatus } from '../entities/Transaction'; import { Agent } from '../entities/Agent'; const walletRepository = AppDataSource.getRepository(Wallet); const transactionRepository = AppDataSource.getRepository(Transaction); /** * Wallet Service * Manages user wallet points and transactions * 1 point = $0.05 */ export class WalletService { private readonly POINTS_PER_DOLLAR = 20; // 1 point = $0.05, so $1 = 20 points private readonly FREE_TASKS_PER_AGENT = 2; /** * Get or create wallet for user */ async getOrCreateWallet(userId: string): Promise { let wallet = await walletRepository.findOne({ where: { userId }, }); if (!wallet) { wallet = walletRepository.create({ userId, balance: 0, freeTasksUsed: 0, }); wallet = await walletRepository.save(wallet); } return wallet; } /** * Get wallet balance */ async getBalance(userId: string): Promise { const wallet = await this.getOrCreateWallet(userId); return Number(wallet.balance); } /** * Add points to wallet (purchase) * Idempotent - checks for existing transaction by reference */ async addPoints( userId: string, points: number, paymentReference: string, metadata?: Record ): Promise { // Check if transaction already exists (idempotency) const existingTx = await transactionRepository.findOne({ where: { paymentReference, type: TransactionType.PURCHASE }, }); if (existingTx && existingTx.status === TransactionStatus.COMPLETED) { console.log(`Transaction ${paymentReference} already processed`); return existingTx; } const wallet = await this.getOrCreateWallet(userId); const newBalance = Number(wallet.balance) + points; // Create transaction const transaction = transactionRepository.create({ walletId: wallet.id, userId, type: TransactionType.PURCHASE, status: TransactionStatus.COMPLETED, amount: points, balanceAfter: newBalance, paymentReference, description: `Purchased ${points} points`, metadata, }); await transactionRepository.save(transaction); // Update wallet balance wallet.balance = newBalance; await walletRepository.save(wallet); return transaction; } /** * Get transaction by payment reference (for idempotency checks) */ async getTransactionByReference(reference: string): Promise { return await transactionRepository.findOne({ where: { paymentReference: reference }, }); } /** * Charge points for agent task * Returns true if charge successful, false if insufficient balance or free task */ async chargeForTask( userId: string, agentId: string, agent: Agent ): Promise<{ success: boolean; transaction?: Transaction; isFree: boolean }> { const wallet = await this.getOrCreateWallet(userId); const pointsRequired = Number(agent.pointsPerTask) || 0; if (pointsRequired === 0) { // Free agent, create free transaction const transaction = transactionRepository.create({ walletId: wallet.id, userId, agentId, type: TransactionType.FREE, status: TransactionStatus.COMPLETED, amount: 0, balanceAfter: Number(wallet.balance), description: `Free task with ${agent.name}`, }); await transactionRepository.save(transaction); return { success: true, transaction, isFree: true }; } // Check free tasks used for this agent // For simplicity, we track per agent in metadata or use a separate table // For now, using first 2 tasks free per user (across all agents) const freeTasksCount = await transactionRepository.count({ where: { userId, type: TransactionType.FREE, }, }); if (freeTasksCount < this.FREE_TASKS_PER_AGENT) { // Free task const transaction = transactionRepository.create({ walletId: wallet.id, userId, agentId, type: TransactionType.FREE, status: TransactionStatus.COMPLETED, amount: 0, balanceAfter: Number(wallet.balance), description: `Free task ${freeTasksCount + 1}/${this.FREE_TASKS_PER_AGENT} with ${agent.name}`, metadata: { agentId, agentName: agent.name }, }); await transactionRepository.save(transaction); return { success: true, transaction, isFree: true }; } // Check balance const currentBalance = Number(wallet.balance); if (currentBalance < pointsRequired) { return { success: false, isFree: false }; } // Charge points const newBalance = currentBalance - pointsRequired; const transaction = transactionRepository.create({ walletId: wallet.id, userId, agentId, type: TransactionType.CHARGE, status: TransactionStatus.COMPLETED, amount: -pointsRequired, // Negative for charges balanceAfter: newBalance, description: `Charged ${pointsRequired} points for ${agent.name} task`, metadata: { agentId, agentName: agent.name, pointsPerTask: pointsRequired }, }); await transactionRepository.save(transaction); // Update wallet balance wallet.balance = newBalance; await walletRepository.save(wallet); return { success: true, transaction, isFree: false }; } /** * Convert dollars to points */ dollarsToPoints(dollars: number): number { return Math.round(dollars * this.POINTS_PER_DOLLAR); } /** * Convert points to dollars */ pointsToDollars(points: number): number { return points / this.POINTS_PER_DOLLAR; } /** * Get user's transaction history */ async getTransactionHistory( userId: string, limit: number = 50 ): Promise { const wallet = await this.getOrCreateWallet(userId); return await transactionRepository.find({ where: { walletId: wallet.id }, relations: ['agent'], order: { createdAt: 'DESC' }, take: limit, }); } /** * Get free tasks remaining for user */ async getFreeTasksRemaining(userId: string): Promise { const freeTasksCount = await transactionRepository.count({ where: { userId, type: TransactionType.FREE, }, }); return Math.max(0, this.FREE_TASKS_PER_AGENT - freeTasksCount); } }