zurri / src /services /walletService.ts
nexusbert's picture
push to space
4a285d2
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<Wallet> {
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<number> {
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<string, any>
): Promise<Transaction> {
// 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<Transaction | null> {
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<Transaction[]> {
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<number> {
const freeTasksCount = await transactionRepository.count({
where: {
userId,
type: TransactionType.FREE,
},
});
return Math.max(0, this.FREE_TASKS_PER_AGENT - freeTasksCount);
}
}