/** * 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', }; } } }