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

350 lines
11 KiB
TypeScript

/**
* 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<string, TransactionCacheEntry>;
transactionDetails: Record<string, TransactionDetailCacheEntry>;
ensureSubscription: (uid: string) => void;
refreshTransactions: (uid: string) => Promise<Transaction[]>;
invalidateTransactions: (uid: string) => Promise<Transaction[]>;
removeTransactions: (uid: string) => void;
ensureTransactionDetail: (transactionId: string) => void;
refreshTransactionDetail: (transactionId: string) => Promise<Transaction | null>;
invalidateTransactionDetail: (transactionId: string) => Promise<Transaction | null>;
removeTransactionDetail: (transactionId: string) => void;
clearAll: () => void;
}
const buildEntry = (
existing: TransactionCacheEntry | undefined,
overrides: Partial<TransactionCacheEntry>
): TransactionCacheEntry => ({
transactions: existing?.transactions ?? [],
loading: existing?.loading ?? false,
error: existing?.error ?? null,
lastFetched: existing?.lastFetched,
...overrides,
});
const buildDetailEntry = (
existing: TransactionDetailCacheEntry | undefined,
overrides: Partial<TransactionDetailCacheEntry>
): 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<TransactionStoreState>((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;
};