attempt to fix test txs
This commit is contained in:
@@ -598,10 +598,14 @@ class PbSyncService {
|
||||
await _syncAppPrefs(userId, filter, prefs);
|
||||
}
|
||||
|
||||
/// Upserts local bank txs to PocketBase. Does **not** delete remote rows
|
||||
/// (partial local caches must not wipe another device).
|
||||
/// Upserts local bank txs to PocketBase.
|
||||
///
|
||||
/// Also garbage-collects remote rows that are:
|
||||
/// - admin test rows (`is_manual` / `·#…` stamp) no longer local, or
|
||||
/// - listed in [deleteTombs] (local delete queue).
|
||||
Future<String> pushTransactions(
|
||||
List<BankTransaction> local, {
|
||||
List<BankTransaction> deleteTombs = const [],
|
||||
void Function(double progress)? onProgress,
|
||||
void Function(String message)? onStatus,
|
||||
double progressStart = 0.0,
|
||||
@@ -630,10 +634,19 @@ class PbSyncService {
|
||||
final ops = <_BatchOp>[];
|
||||
var skipped = 0;
|
||||
final seen = <String>{};
|
||||
final tombIds = {
|
||||
for (final t in deleteTombs)
|
||||
if (_clip(t.id, 2000).isNotEmpty) _clip(t.id, 2000),
|
||||
};
|
||||
|
||||
for (final tx in local) {
|
||||
final syncId = _clip(tx.id, 2000);
|
||||
if (syncId.isEmpty || !seen.add(syncId)) continue;
|
||||
// Never re-upsert a tombstoned row in the same pass.
|
||||
if (tombIds.contains(syncId) ||
|
||||
deleteTombs.any(tx.matchesForDelete)) {
|
||||
continue;
|
||||
}
|
||||
final body = <String, dynamic>{
|
||||
'user': userId,
|
||||
'tx_id': syncId,
|
||||
@@ -648,7 +661,9 @@ class PbSyncService {
|
||||
8,
|
||||
),
|
||||
'pending': tx.pending,
|
||||
'is_manual': tx.isManual,
|
||||
// Only send when true so servers without the migration still accept
|
||||
// normal bank/CSV upserts. Test rows are also detected via ·# stamp.
|
||||
if (tx.isManual) 'is_manual': true,
|
||||
};
|
||||
final remote = existing[syncId];
|
||||
if (remote == null) {
|
||||
@@ -660,11 +675,25 @@ class PbSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove admin test / tombstoned rows that this device no longer has.
|
||||
var deletes = 0;
|
||||
for (final entry in existing.entries) {
|
||||
if (seen.contains(entry.key)) continue;
|
||||
final remoteTx = _bankTxFromRecord(entry.value);
|
||||
final tombHit = tombIds.contains(entry.key) ||
|
||||
(remoteTx != null && deleteTombs.any(remoteTx.matchesForDelete));
|
||||
final orphanManual = _remoteLooksLikeManualTest(entry.value);
|
||||
if (!tombHit && !orphanManual) continue;
|
||||
ops.add(_BatchOp.delete(entry.value.id));
|
||||
deletes++;
|
||||
}
|
||||
|
||||
final creates = ops.where((o) => o.kind == _BatchOpKind.create).length;
|
||||
final updates = ops.where((o) => o.kind == _BatchOpKind.update).length;
|
||||
debugPrint(
|
||||
'PbSyncService transactions: local=${local.length} '
|
||||
'remote=${existing.length} create=$creates update=$updates skip=$skipped',
|
||||
'remote=${existing.length} create=$creates update=$updates '
|
||||
'delete=$deletes skip=$skipped',
|
||||
);
|
||||
onStatus?.call(
|
||||
ops.isEmpty
|
||||
@@ -678,8 +707,13 @@ class PbSyncService {
|
||||
onStatus: onStatus,
|
||||
);
|
||||
return 'Bank txs: local ${local.length} → remote '
|
||||
'${existing.length + creates} '
|
||||
'(+$creates ~$updates · $skipped unchanged)';
|
||||
'${existing.length + creates - deletes} '
|
||||
'(+$creates ~$updates -$deletes · $skipped unchanged)';
|
||||
}
|
||||
|
||||
bool _remoteLooksLikeManualTest(RecordModel r) {
|
||||
if (r.getBoolValue('is_manual')) return true;
|
||||
return RegExp(r' ·#\d+$').hasMatch(r.getStringValue('description'));
|
||||
}
|
||||
|
||||
BankTransaction? _bankTxFromRecord(RecordModel r) {
|
||||
@@ -700,7 +734,8 @@ class PbSyncService {
|
||||
? 'EUR'
|
||||
: r.getStringValue('currency'),
|
||||
pending: r.getBoolValue('pending'),
|
||||
isManual: r.getBoolValue('is_manual'),
|
||||
isManual: r.getBoolValue('is_manual') ||
|
||||
RegExp(r' ·#\d+$').hasMatch(description),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -711,6 +746,8 @@ class PbSyncService {
|
||||
String s => double.tryParse(s) ?? 0,
|
||||
_ => 0.0,
|
||||
};
|
||||
final remoteManual = remote.getBoolValue('is_manual') ||
|
||||
RegExp(r' ·#\d+$').hasMatch(remote.getStringValue('description'));
|
||||
return remote.getStringValue('date') == tx.date &&
|
||||
remote.getStringValue('description') ==
|
||||
_clip(tx.description.isEmpty ? 'Unknown' : tx.description, 4000) &&
|
||||
@@ -718,29 +755,53 @@ class PbSyncService {
|
||||
remote.getStringValue('currency').toUpperCase() ==
|
||||
tx.currency.toUpperCase() &&
|
||||
remote.getBoolValue('pending') == tx.pending &&
|
||||
remote.getBoolValue('is_manual') == tx.isManual;
|
||||
remoteManual == tx.isManual;
|
||||
}
|
||||
|
||||
/// Deletes remote bank rows by local [BankTransaction.id] (tx_id field).
|
||||
Future<String> deleteTransactionsByTxIds(Iterable<String> txIds) async {
|
||||
final ids = {
|
||||
for (final id in txIds)
|
||||
if (_clip(id, 2000).isNotEmpty) _clip(id, 2000),
|
||||
};
|
||||
if (ids.isEmpty) return 'Bank txs: nothing to delete';
|
||||
/// Deletes remote bank rows matching [tombstones] by tx_id or field fingerprint.
|
||||
///
|
||||
/// Returns the tombstones that matched at least one remote row (safe to clear).
|
||||
Future<List<BankTransaction>> deleteTransactionsMatching(
|
||||
List<BankTransaction> tombstones,
|
||||
) async {
|
||||
if (tombstones.isEmpty) return const [];
|
||||
|
||||
final userId = _userId;
|
||||
final filter = 'user="$userId"';
|
||||
final existing = await _fullList('transactions', filter);
|
||||
final ops = <_BatchOp>[
|
||||
for (final r in existing)
|
||||
if (ids.contains(r.getStringValue('tx_id'))) _BatchOp.delete(r.id),
|
||||
];
|
||||
final matchedTombs = <BankTransaction>[];
|
||||
final ops = <_BatchOp>[];
|
||||
final seenRecordIds = <String>{};
|
||||
|
||||
for (final r in existing) {
|
||||
final remoteTx = _bankTxFromRecord(r);
|
||||
if (remoteTx == null) continue;
|
||||
for (final tomb in tombstones) {
|
||||
final txId = r.getStringValue('tx_id');
|
||||
final idHit = txId == _clip(tomb.id, 2000);
|
||||
if (!idHit && !remoteTx.matchesForDelete(tomb)) continue;
|
||||
if (!seenRecordIds.add(r.id)) break;
|
||||
ops.add(_BatchOp.delete(r.id));
|
||||
if (!matchedTombs.any(tomb.matchesForDelete)) {
|
||||
matchedTombs.add(tomb);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ops.isEmpty) {
|
||||
return 'Bank txs: 0 remote deletes (ids ${ids.length})';
|
||||
debugPrint(
|
||||
'PbSyncService deleteTransactionsMatching: 0 hits '
|
||||
'for ${tombstones.length} tombstone(s)',
|
||||
);
|
||||
return const [];
|
||||
}
|
||||
await _runWriteOps('transactions', ops);
|
||||
return 'Bank txs: deleted ${ops.length} remote row(s)';
|
||||
debugPrint(
|
||||
'PbSyncService deleteTransactionsMatching: deleted ${ops.length} '
|
||||
'for ${matchedTombs.length} tombstone(s)',
|
||||
);
|
||||
return matchedTombs;
|
||||
}
|
||||
|
||||
Future<void> _runOne(String collection, _BatchOp op) {
|
||||
|
||||
Reference in New Issue
Block a user