Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/core/error/error_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ class ErrorHandler {
}

if (err.type == DioExceptionType.unknown) {
_recordApiError(err);
return UnknownException('Unknown error');
}

final statusCode = err.response?.statusCode;
final responseDataForMessage = err.response?.data;
final message = responseDataForMessage is Map
Expand Down
11 changes: 0 additions & 11 deletions lib/data/datasources/budget/budget_local_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:trakli/core/utils/date_util.dart';
import 'package:trakli/core/utils/id_helper.dart';
import 'package:trakli/data/services/budget/budget_progress_recomputer.dart';
import 'package:trakli/data/database/app_database.dart';
import 'package:trakli/domain/entities/budget_progress_entity.dart';
import 'package:trakli/presentation/utils/enums.dart';

class BudgetTargetInput {
Expand Down Expand Up @@ -76,9 +75,6 @@ abstract class BudgetLocalDataSource {

Future<List<ResolvedBudgetTarget>> getResolvedTargetsForBudget(
String budgetClientId);

Future<void> updateBudgetProgressByServerId(
int id, BudgetProgressEntity progress);
}

@Injectable(as: BudgetLocalDataSource)
Expand Down Expand Up @@ -335,13 +331,6 @@ class BudgetLocalDataSourceImpl implements BudgetLocalDataSource {
return out;
}

@override
Future<void> updateBudgetProgressByServerId(
int id, BudgetProgressEntity progress) async {
await (database.update(database.budgets)..where((b) => b.id.equals(id)))
.write(BudgetsCompanion(progress: Value(progress)));
}

@override
Future<Budget> deleteBudget(String clientId) async {
final row = await (database.select(database.budgets)
Expand Down
41 changes: 4 additions & 37 deletions lib/data/datasources/budget/budget_remote_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import 'package:trakli/core/utils/date_util.dart';
import 'package:trakli/core/utils/json_defaults.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_period_state_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart';
import 'package:trakli/data/datasources/core/api_response.dart';
import 'package:trakli/data/datasources/core/pagination_response.dart';

Expand All @@ -19,11 +17,6 @@ abstract class BudgetRemoteDataSource {
Future<BudgetCompleteDto> insertBudget(BudgetCompleteDto dto);
Future<BudgetCompleteDto> updateBudget(BudgetCompleteDto dto);
Future<void> deleteBudget(int id);
Future<BudgetProgressDto?> getBudgetProgress(int id);
Future<BudgetTransactionsResponse?> getBudgetTransactions(
int id, {
int limit = 50,
});
Future<BudgetCompleteDto?> closeBudgetPeriod(int id);
Future<List<BudgetPeriodStateDto>> getAllPeriodStates({
DateTime? syncedSince,
Expand All @@ -48,10 +41,10 @@ class BudgetRemoteDataSourceImpl implements BudgetRemoteDataSource {

while (true) {
final queryParams = <String, dynamic>{'page': currentPage};
if (syncedSince != null) {
queryParams['synced_since'] =
formatServerIsoDateTimeString(syncedSince);
}
// if (syncedSince != null) {
// queryParams['synced_since'] =
// formatServerIsoDateTimeString(syncedSince);
// }
if (noClientId != null) {
queryParams['no_client_id'] = noClientId;
}
Expand Down Expand Up @@ -114,32 +107,6 @@ class BudgetRemoteDataSourceImpl implements BudgetRemoteDataSource {
await dio.delete('budgets/$id');
}

@override
Future<BudgetProgressDto?> getBudgetProgress(int id) async {
final response = await dio.get('budgets/$id/progress');
if (response.data == null) return null;
final apiResponse = ApiResponse.fromJson(response.data);
return BudgetProgressDto.fromJson(
apiResponse.data as Map<String, dynamic>,
);
}

@override
Future<BudgetTransactionsResponse?> getBudgetTransactions(
int id, {
int limit = 50,
}) async {
final response = await dio.get(
'budgets/$id/transactions',
queryParameters: {'limit': limit},
);
if (response.data == null) return null;
final apiResponse = ApiResponse.fromJson(response.data);
return BudgetTransactionsResponse.fromJson(
apiResponse.data as Map<String, dynamic>,
);
}

@override
Future<BudgetCompleteDto?> closeBudgetPeriod(int id) async {
final response = await dio.post('budgets/$id/close-period');
Expand Down
30 changes: 0 additions & 30 deletions lib/data/datasources/budget/dtos/budget_transactions_response.dart

This file was deleted.

18 changes: 0 additions & 18 deletions lib/data/mappers/budget_mapper.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:trakli/data/database/app_database.dart' as db;
import 'package:trakli/data/datasources/budget/dtos/budget_progress_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart';
import 'package:trakli/domain/entities/budget_entity.dart';
import 'package:trakli/domain/entities/budget_period_state_entity.dart';
import 'package:trakli/domain/entities/budget_progress_entity.dart';
Expand Down Expand Up @@ -38,23 +37,6 @@ class BudgetMapper {
);
}

static BudgetTargetEntity targetFromDb(db.BudgetTarget row, {String? name}) {
return BudgetTargetEntity(
type: row.targetType,
clientId: row.targetClientId,
name: name,
);
}

static BudgetTargetEntity targetFromDto(BudgetTargetDto dto) {
return BudgetTargetEntity(
type: dto.type,
id: dto.id,
clientId: dto.clientId,
name: dto.name,
);
}

static BudgetPeriodStateEntity periodStateToDomain(db.BudgetPeriodState row) {
return BudgetPeriodStateEntity(
clientId: row.clientId,
Expand Down
24 changes: 0 additions & 24 deletions lib/data/repositories/budget_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import 'package:trakli/data/datasources/budget/budget_local_datasource.dart';
import 'package:trakli/data/datasources/budget/budget_remote_datasource.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_complete_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_target_dto.dart';
import 'package:trakli/data/datasources/budget/dtos/budget_transactions_response.dart';
import 'package:trakli/data/mappers/budget_mapper.dart';
import 'package:trakli/data/sync/budget_sync_handler.dart';
import 'package:trakli/domain/entities/budget_entity.dart';
import 'package:trakli/domain/entities/budget_period_state_entity.dart';
import 'package:trakli/domain/entities/budget_progress_entity.dart';
import 'package:trakli/domain/entities/budget_target_entity.dart';
import 'package:trakli/domain/repositories/budget_repository.dart';
import 'package:trakli/presentation/utils/enums.dart';
Expand Down Expand Up @@ -205,28 +203,6 @@ class BudgetRepositoryImpl
});
}

@override
Future<Either<Failure, BudgetProgressEntity?>> fetchBudgetProgress(int id) {
return RepositoryErrorHandler.handleApiCall(() async {
final dto = await remoteDataSource.getBudgetProgress(id);
if (dto == null) return null;
final progress = BudgetMapper.progressFromDto(dto);

await localDataSource.updateBudgetProgressByServerId(id, progress);

return progress;
});
}

@override
Future<Either<Failure, BudgetTransactionsResponse?>> fetchBudgetTransactions(
int id,
{int limit = 50}) {
return RepositoryErrorHandler.handleApiCall(() async {
return remoteDataSource.getBudgetTransactions(id, limit: limit);
});
}

@override
Future<Either<Failure, Unit>> closeBudgetPeriod(int id) {
return RepositoryErrorHandler.handleApiCall(() async {
Expand Down
16 changes: 2 additions & 14 deletions lib/data/services/budget/budget_progress_recomputer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import 'package:trakli/domain/entities/exchange_rate_entity.dart';
import 'package:trakli/domain/repositories/exchange_rate_repository.dart';
import 'package:trakli/presentation/utils/enums.dart';

/// Computes and persists local budget progress, writing the result to the
/// `budgets.progress` column. Server reconciliation later overwrites it.
// Server reconciliation later overwrites the persisted progress.
@lazySingleton
class BudgetProgressRecomputer {
BudgetProgressRecomputer(this._db, this._exchangeRateRepository);
Expand All @@ -19,23 +18,19 @@ class BudgetProgressRecomputer {

StreamSubscription<ExchangeRateEntity>? _fxSub;

/// Recomputes all budgets whenever a fresh rate is cached, re-folding
/// foreign-currency transactions that an earlier offline recompute excluded.
/// Call once at app boot; idempotent.
// FX self-heal: re-fold foreign-currency txns excluded while offline once a rate is cached.
void attachFxSelfHeal() {
if (_fxSub != null) return;
_fxSub = _exchangeRateRepository.onExchangeRateUpdated.listen((_) {
recomputeAll();
});
}

/// Cancels the self-heal subscription (mainly for tests).
Future<void> dispose() async {
await _fxSub?.cancel();
_fxSub = null;
}

/// Recompute progress for a single budget by its client id.
Future<void> recomputeFor(String budgetClientId) async {
final budget = await (_db.select(_db.budgets)
..where((b) => b.clientId.equals(budgetClientId)))
Expand All @@ -44,8 +39,6 @@ class BudgetProgressRecomputer {
await _recomputeAndWrite(budget);
}

/// Recompute every active budget affected by the given transaction (empty
/// targets = catch-all, else a target must match its wallet/group/category).
Future<void> recomputeAffectedBy({
required String walletClientId,
String? groupClientId,
Expand Down Expand Up @@ -74,7 +67,6 @@ class BudgetProgressRecomputer {
}
}

/// Recompute progress for every active budget.
Future<void> recomputeAll() async {
final activeBudgets = await (_db.select(_db.budgets)
..where((b) => b.isActive.equals(true)))
Expand All @@ -90,8 +82,6 @@ class BudgetProgressRecomputer {
}) async {
final ts = targets ?? await _targetsFor(budget.clientId);
final txns = await _txnInputs();
// Cache-only, offline-safe read; when absent, foreign-currency txns are
// excluded until a rate is cached (see attachFxSelfHeal).
final exchangeRate = await _exchangeRateRepository.getCachedExchangeRate();
final progress = computeLocalProgress(
budget: budget,
Expand All @@ -112,7 +102,6 @@ class BudgetProgressRecomputer {
.get();
}

/// Joins transactions with their category client ids and wallet currency.
Future<List<BudgetTxnInput>> _txnInputs() async {
final txns = await _db.select(_db.transactions).get();
if (txns.isEmpty) return const [];
Expand All @@ -128,7 +117,6 @@ class BudgetProgressRecomputer {
.add(row.categoryClientId);
}

// One-shot wallet → currency lookup so the per-txn cost stays O(1).
final wallets = await _db.select(_db.wallets).get();
final currencyByWallet = {
for (final w in wallets) w.clientId: w.currency,
Expand Down
8 changes: 1 addition & 7 deletions lib/data/services/budget/compute_local_progress.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:trakli/domain/entities/budget_progress_entity.dart';
import 'package:trakli/domain/entities/exchange_rate_entity.dart';
import 'package:trakli/presentation/utils/enums.dart';

/// A transaction enriched with the category client ids it is tagged with.
class BudgetTxnInput {
final TransactionType type;
final double amount;
Expand Down Expand Up @@ -74,7 +73,6 @@ BudgetProgressEntity computeLocalProgress({
? null
: _convert(t.amount, txnCurrency, budget.currency, exchangeRate);
if (converted == null) {
// No usable rate — exclude until one is cached (see attachFxSelfHeal).
continue;
}
grossSpent += converted;
Expand Down Expand Up @@ -119,17 +117,14 @@ BudgetProgressEntity computeLocalProgress({
effectiveLimit: effectiveLimit,
remaining: remaining,
percentUsed: percentUsed,
// Projection/forecast are server-only signals; default locally.
projectedSpend: netSpent,
status: status,
isThresholdCrossed: isThresholdCrossed,
isForecastBreach: false,
);
}

/// Converts [amount] from [from] to [to] via the base-relative snapshot
/// (`amount / rate(from) * rate(to)`). Returns `null` when a rate is missing
/// or the source rate is zero, so the caller can exclude the transaction.
// FX conversion via base-relative snapshot: amount / rate(from) * rate(to).
double? _convert(double amount, String from, String to, ExchangeRateEntity fx) {
if (from == to) return amount;
final rateFrom = from == fx.baseCode ? 1.0 : fx.rates[from];
Expand All @@ -152,7 +147,6 @@ BudgetStatus _deriveStatus({
}

(DateTime, DateTime) _periodWindow(Budget budget, DateTime now) {
// Clamp the reference to start_date so pre-start dates still yield a window.
final ref = now.isBefore(budget.startDate) ? budget.startDate : now;

switch (budget.periodType) {
Expand Down
5 changes: 1 addition & 4 deletions lib/data/services/budget/period_state_client_id.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
/// Deterministic local client id for a server-authored `BudgetPeriodState`,
/// minted from its immutable server `id` so re-syncs `insertOrReplace`
/// idempotently. The `bps:server-` prefix distinguishes these from real
/// device-scoped client ids.
// Local client id derived from the server id.
String periodStateClientId(int serverId) => 'bps:server-$serverId';
Loading
Loading