324 lines
10 KiB
TypeScript
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',
|
|
};
|
|
}
|
|
}
|
|
}
|