fix pending transaction
This commit is contained in:
@@ -27,7 +27,31 @@ bool isSameTransaction(BankTransaction a, BankTransaction b) {
|
||||
if (a.date != b.date) return false;
|
||||
if (a.currency.toUpperCase() != b.currency.toUpperCase()) return false;
|
||||
if (_amountCents(a.amount) != _amountCents(b.amount)) return false;
|
||||
return _descriptionsLikelyMatch(a, b);
|
||||
}
|
||||
|
||||
/// True when a cached/API [pending] row is the unsettled form of [booked].
|
||||
///
|
||||
/// Banks often change booking date (and sometimes wording) when a payment
|
||||
/// settles, so this allows a small date window unlike [isSameTransaction].
|
||||
bool isPendingSettledBy(
|
||||
BankTransaction pending,
|
||||
BankTransaction booked, {
|
||||
int maxDayGap = 10,
|
||||
}) {
|
||||
if (!pending.pending || booked.pending) return false;
|
||||
if (pending.isManual || booked.isManual) return false;
|
||||
if (pending.currency.toUpperCase() != booked.currency.toUpperCase()) {
|
||||
return false;
|
||||
}
|
||||
if (_amountCents(pending.amount) != _amountCents(booked.amount)) return false;
|
||||
if (!_descriptionsLikelyMatch(pending, booked)) return false;
|
||||
final gap = _dateDiffDays(pending.date, booked.date);
|
||||
if (gap == null) return false;
|
||||
return gap.abs() <= maxDayGap;
|
||||
}
|
||||
|
||||
bool _descriptionsLikelyMatch(BankTransaction a, BankTransaction b) {
|
||||
if (a.descriptionKey == b.descriptionKey) return true;
|
||||
|
||||
final ma = a.merchantKey;
|
||||
@@ -47,10 +71,62 @@ bool isSameTransaction(BankTransaction a, BankTransaction b) {
|
||||
|
||||
int _amountCents(double amount) => (amount * 100).round();
|
||||
|
||||
int? _dateDiffDays(String a, String b) {
|
||||
final da = DateTime.tryParse(a);
|
||||
final db = DateTime.tryParse(b);
|
||||
if (da == null || db == null) return null;
|
||||
return da.difference(db).inDays;
|
||||
}
|
||||
|
||||
/// Coarse bucket so fuzzy [isSameTransaction] only compares likely peers.
|
||||
String _dedupeBucket(BankTransaction tx) =>
|
||||
'${tx.date}|${tx.currency.toUpperCase()}|${_amountCents(tx.amount)}';
|
||||
|
||||
String _amountCurrencyBucket(BankTransaction tx) =>
|
||||
'${tx.currency.toUpperCase()}|${_amountCents(tx.amount)}';
|
||||
|
||||
/// Drop pending rows that already have a booked twin (same money + merchant).
|
||||
List<BankTransaction> dropSettledPendingDuplicates(
|
||||
List<BankTransaction> txs,
|
||||
) {
|
||||
final booked = [for (final tx in txs) if (!tx.pending) tx];
|
||||
if (booked.isEmpty) return txs;
|
||||
|
||||
final bookedBuckets = <String, List<BankTransaction>>{};
|
||||
for (final tx in booked) {
|
||||
bookedBuckets.putIfAbsent(_amountCurrencyBucket(tx), () => []).add(tx);
|
||||
}
|
||||
|
||||
final out = <BankTransaction>[];
|
||||
for (final tx in txs) {
|
||||
if (!tx.pending || tx.isManual) {
|
||||
out.add(tx);
|
||||
continue;
|
||||
}
|
||||
final peers = bookedBuckets[_amountCurrencyBucket(tx)];
|
||||
final settled =
|
||||
peers != null && peers.any((b) => isPendingSettledBy(tx, b));
|
||||
if (!settled) out.add(tx);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Merge a fresh bank API page with locally cached history.
|
||||
///
|
||||
/// Pending rows are ephemeral: only the current API list may keep them
|
||||
/// (except admin manuals). That stops settled/cancelled pendings from
|
||||
/// living forever in the on-device cache after the bank drops them.
|
||||
List<BankTransaction> mergeApiTransactionsWithCache(
|
||||
List<BankTransaction> apiTxs,
|
||||
List<BankTransaction> cached,
|
||||
) {
|
||||
final history = [
|
||||
for (final tx in cached)
|
||||
if (!tx.pending || tx.isManual) tx,
|
||||
];
|
||||
return dropSettledPendingDuplicates(mergeTransactions(apiTxs, history));
|
||||
}
|
||||
|
||||
/// Merge [extra] into [existing], skipping duplicates. Newest date first.
|
||||
List<BankTransaction> mergeTransactions(
|
||||
List<BankTransaction> existing,
|
||||
|
||||
Reference in New Issue
Block a user