Skip to content

Feat/add transfer#188

Merged
nfebe merged 15 commits intodevfrom
feat/add-transfer
Mar 22, 2026
Merged

Feat/add transfer#188
nfebe merged 15 commits intodevfrom
feat/add-transfer

Conversation

@austin047
Copy link
Copy Markdown
Collaborator

@austin047 austin047 commented Mar 19, 2026

Description

Add transfer

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

@austin047 austin047 marked this pull request as ready for review March 19, 2026 10:16
@sourceant
Copy link
Copy Markdown

sourceant Bot commented Mar 19, 2026

Code Review Summary

The PR adds the ability to transfer money between wallets. It includes database schema changes (v4), migration steps, DTOs, repository implementations, and a new UI for performing and viewing transfers. It also introduces paged/streaming sync capabilities to the synchronization core.

🚀 Key Improvements

  • Added Transfers table and logic to atomically link expense and income transactions.
  • Implemented PagedSyncTypeHandler to allow incremental, memory-efficient syncing of transactions.
  • Improved TransactionSyncHandler to preserve transfer links during local upserts.

💡 Minor Suggestions

  • Reduce code duplication in FeatureRemoteConfig and UI formatting logic.
  • Use more robust error handling for fire-and-forget sync operations.

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

);
}

if (selectedFromWallet != null && amount > selectedFromWallet!.balance) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'amountMustNotBeZero' is used when the amount exceeds the balance. This is semantically incorrect and confusing for the user.

Suggested change
if (selectedFromWallet != null && amount > selectedFromWallet!.balance) {
if (selectedFromWallet != null && amount > selectedFromWallet!.balance) {
return LocaleKeys.insufficientBalance.tr();
}

queryParams['no_client_id'] = noClientId;
}

queryParams['limit'] = limit;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded 'limit' of 10 might be too small for initial syncs. Consider making this a configurable constant or using a higher default.

Suggested change
queryParams['limit'] = limit;
queryParams['limit'] = 50;

Comment on lines +182 to +194
expenseDto = await transactionLocalDataSource.updateTransaction(
expenseDto.transaction.clientId,
transferClientId: transferClientId,
);

incomeDto = await transactionLocalDataSource.updateTransaction(
incomeDto.transaction.clientId,
transferClientId: transferClientId,
);


});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transactions are updated outside of the Drift transaction block. If an error occurs between the local update and the sync call, the data integrity between the transfer record and transactions might be compromised.

Suggested change
expenseDto = await transactionLocalDataSource.updateTransaction(
expenseDto.transaction.clientId,
transferClientId: transferClientId,
);
incomeDto = await transactionLocalDataSource.updateTransaction(
incomeDto.transaction.clientId,
transferClientId: transferClientId,
);
});
expenseDto = await transactionLocalDataSource.updateTransaction(
expenseDto.transaction.clientId,
transferClientId: transferClientId,
);
incomeDto = await transactionLocalDataSource.updateTransaction(
incomeDto.transaction.clientId,
transferClientId: transferClientId,
);
});

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

);
}
if (selectedFromWallet != null && number > selectedFromWallet!.balance) {
return LocaleKeys.amountMustNotBeZero.tr(); // fallback until locale keys update
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'amountMustNotBeZero' is being used for insufficient balance, which is confusing for the user.

Suggested change
return LocaleKeys.amountMustNotBeZero.tr(); // fallback until locale keys update
return LocaleKeys.insufficientBalance.tr();

throw UnimplementedError();
}

@override
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock implementation of getAllTransactionsStream throws UnimplementedError. This will break unit tests or any functionality relying on this mock if the streaming sync is enabled.

Suggested change
@override
@override
Stream<List<TransactionCompleteDto>> getAllTransactionsStream({DateTime? syncedSince, bool? noClientId}) async* {
yield [];
}

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

}
}

Map<String, dynamic> toServerJson(Transfer transfer) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toServerJson helper function should probably be part of the TransferDto or the Transfer data class to keep serialization logic centralized, similar to other entities in the project.

Suggested change
Map<String, dynamic> toServerJson(Transfer transfer) {
// Move this logic into a marshal/toJson method within the DTO or entity mapping layer

Comment thread lib/presentation/home_screen.dart Outdated

transactionsToSort.sort(
(a, b) => b.transaction.datetime.compareTo(a.transaction.datetime),
(a, b) => b.transaction.createdAt.compareTo(a.transaction.createdAt),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting transactions by createdAt instead of datetime might lead to confusing UI if the user manually changes the transaction date to a past or future date. Users usually expect transactions to be sorted by the logical 'datetime' of the event.

Suggested change
(a, b) => b.transaction.createdAt.compareTo(a.transaction.createdAt),
(a, b) => b.transaction.datetime.compareTo(a.transaction.datetime),

Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

@override
Future<Transfer> insertTransfer(TransfersCompanion companion) async {
final now = getNewFormattedUtcDateTime();
final clientId = companion.clientId.present && companion.clientId.value.isNotEmpty
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unawaited or just not awaiting generateDeviceScopedId() is risky if the ID generation has side effects or relies on async state. Ensure IDs are fully generated before insertion.

Suggested change
final clientId = companion.clientId.present && companion.clientId.value.isNotEmpty
final String clientId;
if (companion.clientId.present && companion.clientId.value.isNotEmpty) {
clientId = companion.clientId.value;
} else {
clientId = await generateDeviceScopedId();
}
final toInsert = companion.copyWith(
clientId: Value(clientId),
createdAt: Value(now),
updatedAt: Value(now),
);
return database.into(database.transfers).insertReturning(toInsert);

_remoteConfig.getInt('updateReminderFrequency');


double get minimumTransferAmount {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parsing logic is repeated. Consider creating a private helper method _getDoubleWithFallback to reduce complexity and improve readability.

Suggested change
double get minimumTransferAmount {
double get minimumTransferAmount => _getDoubleWithFallback('minimumTransferAmount', defaultMinimumTransferAmount);
double _getDoubleWithFallback(String key, double fallback) {
try {
final val = _remoteConfig.getDouble(key);
if (val > 0) return val;
} catch (_) {}
try {
final s = _remoteConfig.getString(key);
if (s.isNotEmpty) {
final parsed = double.tryParse(s.replaceAll(',', '.'));
if (parsed != null && parsed > 0) return parsed;
}
} catch (_) {}
return fallback;
}

- Transfers table and Drift schema v4
- transaction transferId, transferClientId columns
- Transaction entity/DTO/mapper include transfer fields
- Transfer DTOs, local/remote datasources, mapper, repository
- Transfer sync handler; sync module and dependency manager
- Transaction local/remote datasource and repo updates for transfers
- Drift sync core updates for transfer entity type
- On transaction upsert, keep existing transferId/transferClientId when server response has null (e.g. offline-created transfer not yet on backend)
- Replace AppUpdateConfig with RemoteFeatureConfig; GetRemoteFeatureConfigUseCase
- Add RemoteConfigCubit; minimum transfer amount from remote config
- Remove get_app_update_config_usecase; check_app_update uses shared config
- Provide RemoteConfigCubit in app_widget
- Transfers list and wallet transfer screens; TransferCubit
- Transfer tile, transaction tile transfer icon, wallet tile link
- Drawer and home screen navigation to transfers; remove old wallet_transfer_screen
- Translation keys and locale files for transfer/min amount
- Regenerate codegen_loader and locale_keys; README update
- Amount parser; register transfer and remote config in injection
- Add PagedSyncTypeHandler; consume remote pages without buffering all items
- TransactionRemoteDataSource: getAllTransactionsStream + delegate getAllTransactions
- TransactionSyncHandler implements streaming; item-by-item upsert in synchronizer
- Mock datasource: stub getAllTransactionsStream
- Add getAllTransfersStream (page + limit); getAllTransfers delegates to stream
- Wrap transfers list in SafeArea
- Use insufficientBalance when amount exceeds wallet balance
- Trim whitespace in transfer repository
Copy link
Copy Markdown

@sourceant sourceant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

Comment thread lib/presentation/transfers/wallet_transfer_screen.dart
Comment thread lib/data/repositories/transfer_repository_impl.dart
Comment thread lib/data/datasources/core/amount_parser.dart
@austin047 austin047 requested a review from nfebe March 22, 2026 03:29
@nfebe nfebe merged commit a86bc74 into dev Mar 22, 2026
5 checks passed
@nfebe nfebe deleted the feat/add-transfer branch March 22, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants