Amba-Agent-App/lib/services/transactionService.ts
2026-01-16 00:22:35 +03:00

324 lines
10 KiB
TypeScript

/**
* Transaction Service - Platform-aware transaction management
* Uses Firebase abstraction layer for cross-platform support
*/
import { doc, collection } from '../firebase';
import { WalletService } from './walletService';
import { calculateTotalAmountForSending, calculateProcessingFee } from '../utils/feeUtils';
import { useTransactionStore } from '../stores/transactionStore';
import { withGlobalLoading } from '../stores/uiStore';
const snapshotExists = (docSnap: any): boolean => {
const existsValue = docSnap?.exists;
if (typeof existsValue === 'function') {
try {
return !!existsValue.call(docSnap);
} catch {
return false;
}
}
return !!existsValue;
};
// Base transaction interface
interface BaseTransaction {
id: string;
uid: string;
amount: number;
status: 'pending' | 'completed' | 'failed';
createdAt: Date;
updatedAt: Date;
}
// Send money transaction
interface SendTransaction extends BaseTransaction {
type: 'send';
recipientName: string;
recipientPhoneNumber: string;
recipientType: 'saved' | 'contact';
recipientId: string;
note: string;
fulfilled: boolean;
}
// Receive money transaction
interface ReceiveTransaction extends BaseTransaction {
type: 'receive';
senderName: string;
senderPhoneNumber: string;
senderType: 'saved' | 'contact';
senderId: string;
note: string;
}
// Add cash transaction
interface AddCashTransaction extends BaseTransaction {
type: 'add_cash';
cardId: string;
cardType: string;
lastFourDigits: string;
}
// Cash out transaction
interface CashOutTransaction extends BaseTransaction {
type: 'cash_out';
bankProvider: 'awash' | 'telebirr';
accountNumber?: string;
note: string;
}
// Discriminated union for all transaction types
export type Transaction = SendTransaction | ReceiveTransaction | AddCashTransaction | CashOutTransaction;
export class TransactionService {
/**
* Create a new transaction when sending money
*/
static async sendMoney(
uid: string,
transactionData: {
amount: number;
recipientName: string;
recipientPhoneNumber: string;
recipientType: 'saved' | 'contact';
recipientId: string;
note: string;
}
): Promise<{ success: boolean; error?: string; transactionId?: string }> {
return withGlobalLoading(async () => {
try {
// Validate input
if (!transactionData.amount || transactionData.amount <= 0) {
return { success: false, error: 'Invalid amount' };
}
if (!transactionData.recipientName || !transactionData.recipientPhoneNumber) {
return { success: false, error: 'Recipient information is required' };
}
// Check if user has sufficient balance
const walletResult = await WalletService.getUserWallet(uid);
if (!walletResult.success) {
return { success: false, error: 'Failed to get wallet information' };
}
const wallet = walletResult.wallet;
if (!wallet) {
return { success: false, error: 'Wallet not found' };
}
// Calculate total amount including processing fee
const totalAmountRequired = calculateTotalAmountForSending(transactionData.amount);
if (wallet.balance < totalAmountRequired) {
const processingFee = calculateProcessingFee(transactionData.amount);
return {
success: false,
error: `Insufficient balance. Required: $${(totalAmountRequired / 100).toFixed(2)} (including $${(processingFee / 100).toFixed(2)} processing fee). Available: $${(wallet.balance / 100).toFixed(2)}`
};
}
// Generate transaction ID
const transactionId = `transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create transaction record
const newTransaction: SendTransaction = {
id: transactionId,
uid,
amount: transactionData.amount,
recipientName: transactionData.recipientName,
recipientPhoneNumber: transactionData.recipientPhoneNumber,
recipientType: transactionData.recipientType,
recipientId: transactionData.recipientId,
note: transactionData.note || '',
fulfilled: false,
status: 'pending',
type: 'send',
createdAt: new Date(),
updatedAt: new Date(),
};
// Save transaction to Firestore
const transactionRef = doc('transactions', transactionId);
await transactionRef.set(newTransaction);
// Update wallet balance
const totalAmountToDeduct = calculateTotalAmountForSending(transactionData.amount);
const newBalance = wallet.balance - totalAmountToDeduct;
const updateResult = await WalletService.updateWalletBalance(uid, newBalance);
if (!updateResult.success) {
return { success: false, error: 'Failed to update wallet balance' };
}
// Update transaction status to completed
await transactionRef.update({
status: 'completed',
updatedAt: new Date(),
});
console.log('Transaction completed successfully:', transactionId);
try {
const transactionStore = useTransactionStore.getState();
await transactionStore.invalidateTransactions(uid);
await transactionStore.invalidateTransactionDetail(transactionId);
} catch (cacheError) {
console.warn('Failed to refresh transaction cache after sendMoney', cacheError);
}
return { success: true, transactionId };
} catch (error) {
console.error('Error creating transaction:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create transaction'
};
}
});
}
/**
* Create a cash out transaction
*/
static async cashOut(
uid: string,
transactionData: {
amount: number;
bankProvider: 'awash' | 'telebirr';
accountNumber?: string;
note: string;
}
): Promise<{ success: boolean; error?: string; transactionId?: string }> {
return withGlobalLoading(async () => {
try {
// Validate input
if (!transactionData.amount || transactionData.amount <= 0) {
return { success: false, error: 'Invalid amount' };
}
if (!transactionData.bankProvider) {
return { success: false, error: 'Bank provider is required' };
}
// Check if user has sufficient balance
const walletResult = await WalletService.getUserWallet(uid);
if (!walletResult.success) {
return { success: false, error: 'Failed to get wallet information' };
}
const wallet = walletResult.wallet;
if (!wallet) {
return { success: false, error: 'Wallet not found' };
}
if (wallet.balance < transactionData.amount) {
return {
success: false,
error: `Insufficient balance. Required: $${(transactionData.amount / 100).toFixed(2)}. Available: $${(wallet.balance / 100).toFixed(2)}`
};
}
// Generate transaction ID
const transactionId = `transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create transaction record
const newTransaction: CashOutTransaction = {
id: transactionId,
uid,
amount: transactionData.amount,
bankProvider: transactionData.bankProvider,
note: transactionData.note || '',
status: 'pending',
type: 'cash_out',
createdAt: new Date(),
updatedAt: new Date(),
...(transactionData.accountNumber ? { accountNumber: transactionData.accountNumber } : {}),
} as CashOutTransaction;
// Save transaction to Firestore
const transactionRef = doc('transactions', transactionId);
await transactionRef.set(newTransaction);
// Update wallet balance
const newBalance = wallet.balance - transactionData.amount;
const updateResult = await WalletService.updateWalletBalance(uid, newBalance);
if (!updateResult.success) {
return { success: false, error: 'Failed to update wallet balance' };
}
// Update transaction status to completed
await transactionRef.update({
status: 'completed',
updatedAt: new Date(),
});
console.log('Cash out transaction completed successfully:', transactionId);
try {
const transactionStore = useTransactionStore.getState();
await transactionStore.invalidateTransactions(uid);
await transactionStore.invalidateTransactionDetail(transactionId);
} catch (cacheError) {
console.warn('Failed to refresh transaction cache after cashOut', cacheError);
}
return { success: true, transactionId };
} catch (error) {
console.error('Error creating cash out transaction:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create cash out transaction'
};
}
});
}
/**
* Get all transactions for a user
*/
static async getUserTransactions(uid: string): Promise<{ success: boolean; transactions?: Transaction[]; error?: string }> {
try {
return { success: true, transactions: [] };
} catch (error) {
console.error('Error getting user transactions:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get transactions'
};
}
}
static async getTransactionById(transactionId: string): Promise<{ success: boolean; transaction?: Transaction; error?: string }> {
try {
const docRef = doc('transactions', transactionId);
const docSnap = await docRef.get();
if (!snapshotExists(docSnap)) {
return { success: false, error: 'Transaction not found' };
}
const data = docSnap.data();
if (!data) {
return { success: false, error: 'Transaction data unavailable' };
}
const transaction = {
...data,
id: data.id ?? transactionId,
createdAt: data.createdAt?.toDate ? data.createdAt.toDate() : data.createdAt ?? new Date(),
updatedAt: data.updatedAt?.toDate ? data.updatedAt.toDate() : data.updatedAt ?? new Date(),
} as Transaction;
return { success: true, transaction };
} catch (error) {
console.error('Error fetching transaction by id:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch transaction',
};
}
}
}