/** * Transaction Store - Platform-aware transaction management * Uses Firebase abstraction layer for cross-platform support */ import { create } from 'zustand'; import { doc, collection } from '../firebase'; import type { Transaction } from '../services/transactionService'; export interface TransactionCacheEntry { transactions: Transaction[]; loading: boolean; error: string | null; lastFetched?: number; } export interface TransactionDetailCacheEntry { transaction: Transaction | null; loading: boolean; error: string | null; lastFetched?: number; } interface TransactionStoreState { transactionsByUid: Record; transactionDetails: Record; ensureSubscription: (uid: string) => void; refreshTransactions: (uid: string) => Promise; invalidateTransactions: (uid: string) => Promise; removeTransactions: (uid: string) => void; ensureTransactionDetail: (transactionId: string) => void; refreshTransactionDetail: (transactionId: string) => Promise; invalidateTransactionDetail: (transactionId: string) => Promise; removeTransactionDetail: (transactionId: string) => void; clearAll: () => void; } const buildEntry = ( existing: TransactionCacheEntry | undefined, overrides: Partial ): TransactionCacheEntry => ({ transactions: existing?.transactions ?? [], loading: existing?.loading ?? false, error: existing?.error ?? null, lastFetched: existing?.lastFetched, ...overrides, }); const buildDetailEntry = ( existing: TransactionDetailCacheEntry | undefined, overrides: Partial ): TransactionDetailCacheEntry => ({ transaction: existing?.transaction ?? null, loading: existing?.loading ?? false, error: existing?.error ?? null, lastFetched: existing?.lastFetched, ...overrides, }); const snapshotExists = (docSnap: any): boolean => { const existsValue = docSnap?.exists; if (typeof existsValue === 'function') { try { return !!existsValue.call(docSnap); } catch { return false; } } return !!existsValue; }; const mapSnapshotToTransactions = (snapshot: any): Transaction[] => { if (!snapshot || !snapshot.docs) { return []; } return snapshot.docs.map((docItem: any) => { const data = docItem.data(); return { ...data, id: data.id ?? docItem.id, createdAt: data.createdAt?.toDate ? data.createdAt.toDate() : data.createdAt ?? new Date(), updatedAt: data.updatedAt?.toDate ? data.updatedAt.toDate() : data.updatedAt ?? new Date(), } as Transaction; }); }; const mapDocumentSnapshotToTransaction = ( transactionId: string, docSnap: any ): Transaction | null => { if (!snapshotExists(docSnap)) { return null; } const data = docSnap.data(); if (!data) { return null; } return { ...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; }; export const useTransactionStore = create((set, get) => ({ transactionsByUid: {}, transactionDetails: {}, ensureSubscription: (uid: string) => { if (!uid) { return; } const existing = get().transactionsByUid[uid]; if (existing?.loading) { return; } if (existing?.lastFetched) { return; } void get().refreshTransactions(uid).catch((error) => { set((state) => ({ transactionsByUid: { ...state.transactionsByUid, [uid]: buildEntry(state.transactionsByUid[uid], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch transactions', }), }, })); }); }, refreshTransactions: async (uid: string) => { if (!uid) { return []; } set((state) => ({ transactionsByUid: { ...state.transactionsByUid, [uid]: buildEntry(state.transactionsByUid[uid], { loading: true, error: null, }), }, })); try { const transactionsCollection = collection('transactions'); const snapshot = await transactionsCollection .where('uid', '==', uid) .orderBy('createdAt', 'desc') .get(); const transactions = mapSnapshotToTransactions(snapshot); set((state) => { const nextDetails = { ...state.transactionDetails }; transactions.forEach((transaction) => { const detailEntry = nextDetails[transaction.id]; if (detailEntry) { nextDetails[transaction.id] = buildDetailEntry(detailEntry, { transaction, loading: false, error: null, lastFetched: Date.now(), }); } }); return { transactionsByUid: { ...state.transactionsByUid, [uid]: buildEntry(state.transactionsByUid[uid], { transactions, loading: false, error: null, lastFetched: Date.now(), }), }, transactionDetails: nextDetails, }; }); return transactions; } catch (error) { set((state) => ({ transactionsByUid: { ...state.transactionsByUid, [uid]: buildEntry(state.transactionsByUid[uid], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch transactions', }), }, })); throw error; } }, invalidateTransactions: async (uid: string) => { if (!uid) { return []; } set((state) => ({ transactionsByUid: { ...state.transactionsByUid, [uid]: buildEntry(state.transactionsByUid[uid], { loading: true, error: null, lastFetched: undefined, }), }, })); return get().refreshTransactions(uid); }, removeTransactions: (uid: string) => { set((state) => { const { [uid]: _removed, ...rest } = state.transactionsByUid; return { transactionsByUid: rest }; }); }, ensureTransactionDetail: (transactionId: string) => { if (!transactionId) { return; } const existing = get().transactionDetails[transactionId]; if (existing?.loading) { return; } if (existing?.lastFetched) { return; } void get().refreshTransactionDetail(transactionId).catch((error) => { set((state) => ({ transactionDetails: { ...state.transactionDetails, [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch transaction', }), }, })); }); }, refreshTransactionDetail: async (transactionId: string) => { if (!transactionId) { return null; } set((state) => ({ transactionDetails: { ...state.transactionDetails, [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { loading: true, error: null, }), }, })); try { const docRef = doc('transactions', transactionId); const docSnap = await docRef.get(); const transaction = mapDocumentSnapshotToTransaction(transactionId, docSnap); set((state) => ({ transactionDetails: { ...state.transactionDetails, [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { transaction, loading: false, error: transaction ? null : 'Transaction not found', lastFetched: Date.now(), }), }, })); return transaction; } catch (error) { set((state) => ({ transactionDetails: { ...state.transactionDetails, [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { loading: false, error: error instanceof Error ? error.message : 'Failed to fetch transaction', }), }, })); throw error; } }, invalidateTransactionDetail: async (transactionId: string) => { if (!transactionId) { return null; } set((state) => ({ transactionDetails: { ...state.transactionDetails, [transactionId]: buildDetailEntry(state.transactionDetails[transactionId], { loading: true, error: null, lastFetched: undefined, }), }, })); return get().refreshTransactionDetail(transactionId); }, removeTransactionDetail: (transactionId: string) => { set((state) => { const { [transactionId]: _removed, ...rest } = state.transactionDetails; return { transactionDetails: rest }; }); }, clearAll: () => { set({ transactionsByUid: {}, transactionDetails: {} }); }, })); export const getTransactionsFromCache = (uid: string): Transaction[] => { if (!uid) { return []; } return useTransactionStore.getState().transactionsByUid[uid]?.transactions ?? []; }; export const getTransactionDetailFromCache = (transactionId: string): Transaction | null => { if (!transactionId) { return null; } return useTransactionStore.getState().transactionDetails[transactionId]?.transaction ?? null; };