From 5889cd6ad9723be19bac4b2018b175fb89954e43 Mon Sep 17 00:00:00 2001 From: Prasanna Anbazhagan Date: Thu, 18 Jan 2024 08:26:07 +0530 Subject: [PATCH] save commit --- lib/domain/models/goal.dart | 21 +++++++++++ lib/domain/models/investment.dart | 22 ++++++++++-- lib/domain/models/irr_calculator.dart | 6 ++-- lib/presentation/goals_presenter.dart | 50 ++++++++++++++++++--------- lib/ui/pages/goals_page.dart | 40 ++++++++++----------- test/domain/irr_calculator_test.dart | 24 +++---------- 6 files changed, 101 insertions(+), 62 deletions(-) diff --git a/lib/domain/models/goal.dart b/lib/domain/models/goal.dart index b9feab2..d6fd68f 100644 --- a/lib/domain/models/goal.dart +++ b/lib/domain/models/goal.dart @@ -107,6 +107,27 @@ class Goal { .then((count) => Void); } + Future getIRR() async { + return _goalInvestmentApi + .getBy(goalId: id) + .then((goalInvestments) => Future.wait(goalInvestments.map( + (goalInvestment) => _investmentApi + .getById(id: goalInvestment.investmentId) + .then((investmentDO) => + Investment.from(investmentDO: investmentDO)) + .then((investment) async => { + 'value': investment.value, + 'irr': await investment.getIRR(), + })))) + .then((investmentData) { + var totalValue = investmentData.fold( + 0.0, (sum, investment) => sum + investment['value']!); + var weightedIRRSum = investmentData.fold(0.0, + (sum, investment) => sum + investment['value']! * investment['irr']!); + return weightedIRRSum / totalValue; + }); + } + static Goal from({required final GoalDO goalDO}) { return Goal( id: goalDO.id, diff --git a/lib/domain/models/investment.dart b/lib/domain/models/investment.dart index 376d720..49f13d0 100644 --- a/lib/domain/models/investment.dart +++ b/lib/domain/models/investment.dart @@ -83,9 +83,6 @@ class Investment { if (value != null && valueUpdatedOn != null) { final irr = _irrCalculator.calculateIRR( payments: payments, value: value!, valueUpdatedOn: valueUpdatedOn!); - if (irr == null) { - return value!; - } return _irrCalculator.calculateValueOnIRR( irr: irr, date: date, value: value!, valueUpdatedOn: valueUpdatedOn!); } else if (irr != null) { @@ -229,6 +226,25 @@ class Investment { .then((count) => Void); } + Future getIRR() async { + if (irr != null) { + return Future(() => irr!); + } else if (value != null && valueUpdatedOn != null) { + List payments = await getTransactions().then((transactions) => + transactions + .map((transaction) => Payment.from(transaction: transaction)) + .toList(growable: true)); + + return getValueOn(date: DateTime.now()).then((valueOn) => + _irrCalculator.calculateIRR( + payments: payments, + value: value!, + valueUpdatedOn: valueUpdatedOn!)); + } else { + return Future.error('IRR is null'); + } + } + static Investment from({required final InvestmentDO investmentDO}) { return Investment( id: investmentDO.id, diff --git a/lib/domain/models/irr_calculator.dart b/lib/domain/models/irr_calculator.dart index 9288cec..b46a76e 100644 --- a/lib/domain/models/irr_calculator.dart +++ b/lib/domain/models/irr_calculator.dart @@ -3,11 +3,11 @@ import 'dart:math'; import 'package:wealth_wave/domain/models/payment.dart'; class IRRCalculator { - double? calculateIRR( + double calculateIRR( {required final List payments, required final double value, required final DateTime valueUpdatedOn}) { - if (payments.isEmpty) return null; + if (payments.isEmpty) return 0.0; payments.sort((a, b) => a.createdOn.compareTo(b.createdOn)); DateTime initialDate = payments.first.createdOn; @@ -33,7 +33,7 @@ class IRRCalculator { if (f.abs() < 1e-6) return guess; // Convergence tolerance guess -= f / df; // Newton-Raphson update } - return null; + return 0.0; } double calculateTransactedValueOnIRR( diff --git a/lib/presentation/goals_presenter.dart b/lib/presentation/goals_presenter.dart index 26df826..6de7d54 100644 --- a/lib/presentation/goals_presenter.dart +++ b/lib/presentation/goals_presenter.dart @@ -1,3 +1,4 @@ +import 'package:wealth_wave/contract/goal_importance.dart'; import 'package:wealth_wave/core/presenter.dart'; import 'package:wealth_wave/domain/models/goal.dart'; import 'package:wealth_wave/domain/models/investment.dart'; @@ -13,23 +14,10 @@ class GoalsPresenter extends Presenter { void fetchGoals() { _goalService .get() - .then((goals) => Future.wait(goals.map((goal) async { - final valueOnMaturity = await goal.getValueOnMaturity(); - final investments = await goal.getInvestments(); - final maturityAmount = await goal.getMaturityAmount(); - - return GoalVO( - id: goal.id, - name: goal.name, - description: goal.description, - goal: goal, - maturityAmount: maturityAmount, - maturityDate: goal.maturityDate, - valueOnMaturity: valueOnMaturity, - investments: investments); - }))) + .then((goals) => + Future.wait(goals.map((goal) => GoalVO.from(goal: goal)))) .then((goalVOs) => - updateViewState((viewState) => viewState.goals = goalVOs)); + updateViewState((viewState) => viewState.goalVOs = goalVOs)); } void deleteGoal({required final int id}) { @@ -38,7 +26,7 @@ class GoalsPresenter extends Presenter { } class GoalsViewState { - List goals = []; + List goalVOs = []; } class GoalVO { @@ -46,11 +34,19 @@ class GoalVO { final String name; final String? description; final double maturityAmount; + final double investedAmount; final DateTime maturityDate; final double valueOnMaturity; + final double inflation; + final double irr; + final GoalImportance importance; final Goal goal; final Map investments; + double get yearsLeft => DateTime.now().difference(maturityDate).inDays / 365; + + double get progress => valueOnMaturity / maturityAmount; + GoalVO( {required this.id, required this.name, @@ -58,6 +54,26 @@ class GoalVO { required this.maturityAmount, required this.maturityDate, required this.valueOnMaturity, + required this.investedAmount, + required this.importance, + required this.inflation, required this.goal, + required this.irr, required this.investments}); + + static Future from({required final Goal goal}) async { + return GoalVO( + id: goal.id, + name: goal.name, + description: goal.description, + goal: goal, + importance: goal.importance, + inflation: goal.inflation, + maturityAmount: await goal.getMaturityAmount(), + investedAmount: await goal.getInvestedAmount(), + maturityDate: goal.maturityDate, + irr: await goal.getIRR(), + valueOnMaturity: await goal.getMaturityAmount(), + investments: await goal.getInvestments()); + } } diff --git a/lib/ui/pages/goals_page.dart b/lib/ui/pages/goals_page.dart index fb849c4..c9845f4 100644 --- a/lib/ui/pages/goals_page.dart +++ b/lib/ui/pages/goals_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:wealth_wave/contract/goal_importance.dart'; import 'package:wealth_wave/core/page_state.dart'; -import 'package:wealth_wave/domain/models/goal.dart'; import 'package:wealth_wave/presentation/goals_presenter.dart'; import 'package:wealth_wave/ui/app_dimen.dart'; import 'package:wealth_wave/ui/widgets/create_goal_dialog.dart'; @@ -24,14 +23,14 @@ class _GoalsPage extends PageState { @override Widget buildWidget(BuildContext context, GoalsViewState snapshot) { - List goals = snapshot.goals; + List goalVOs = snapshot.goalVOs; return Scaffold( body: Center( child: ListView.builder( - itemCount: goals.length, + itemCount: goalVOs.length, itemBuilder: (context, index) { - Goal goal = goals[index]; - return _goalWidget(context: context, goal: goal); + GoalVO goalVO = goalVOs[index]; + return _goalWidget(context: context, goalVO: goalVO); }, )), floatingActionButton: FloatingActionButton( @@ -51,7 +50,7 @@ class _GoalsPage extends PageState { } Widget _goalWidget( - {required final BuildContext context, required final Goal goal}) { + {required final BuildContext context, required final GoalVO goalVO}) { return Card( margin: const EdgeInsets.all(AppDimen.defaultPadding), child: Padding( @@ -64,21 +63,22 @@ class _GoalsPage extends PageState { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text(goal.name, + Text(goalVO.name, style: Theme.of(context).textTheme.titleMedium), const Text(' | '), - Text('${_getImportance(goal.importance)} Importance', + Text('${_getImportance(goalVO.importance)} Importance', style: Theme.of(context).textTheme.labelSmall), const Text(' | '), - Text('(${_getYearsLeft(goal.getYearsLeft())})', + Text('(${_getYearsLeft(goalVO.yearsLeft)})', style: Theme.of(context).textTheme.labelSmall), PopupMenuButton( onSelected: (value) { if (value == 1) { - showCreateGoalDialog(context: context, goal: goal) + showCreateGoalDialog( + context: context, goal: goalVO.goal) .then((value) => presenter.fetchGoals()); } else if (value == 2) { - presenter.deleteGoal(id: goal.id); + presenter.deleteGoal(id: goalVO.id); } }, itemBuilder: (context) => [ @@ -97,11 +97,11 @@ class _GoalsPage extends PageState { TextButton( onPressed: () { showTaggedInvestmentDialog( - context: context, goalId: goal.id) + context: context, goalId: goalVO.id) .then((value) => presenter.fetchGoals()); }, - child: Text( - '${goal.taggedInvestments.length} tagged investments'), + child: + Text('${goalVO.investments.length} tagged investments'), ), ], ), @@ -111,7 +111,7 @@ class _GoalsPage extends PageState { Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(formatToCurrency(goal.getInvestedValue()), + Text(formatToCurrency(goalVO.investedAmount), style: Theme.of(context).textTheme.bodyMedium), Text('(Invested Amount)', style: Theme.of(context).textTheme.labelSmall), @@ -121,10 +121,10 @@ class _GoalsPage extends PageState { Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(formatToCurrency(goal.getFutureValueOnTargetDate()), + Text(formatToCurrency(goalVO.valueOnMaturity), style: Theme.of(context).textTheme.bodyMedium), Text( - '(At growth Rate of ${formatToPercentage(goal.getIrr())})', + '(At growth Rate of ${formatToPercentage(goalVO.irr)})', style: Theme.of(context).textTheme.labelSmall), ], ), @@ -133,17 +133,17 @@ class _GoalsPage extends PageState { padding: const EdgeInsets.all(AppDimen.defaultPadding), child: LinearProgressIndicator( - value: goal.getProgress(), + value: goalVO.progress, semanticsLabel: 'Investment progress', ))), Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text(formatToCurrency(goal.targetAmount), + Text(formatToCurrency(goalVO.maturityAmount), style: Theme.of(context).textTheme.bodyMedium), Text('(Target Amount)', style: Theme.of(context).textTheme.labelSmall), - Text(formatToPercentage(goal.inflation / 100), + Text(formatToPercentage(goalVO.inflation / 100), style: Theme.of(context).textTheme.bodyMedium), Text('(Inflation)', style: Theme.of(context).textTheme.labelSmall), diff --git a/test/domain/irr_calculator_test.dart b/test/domain/irr_calculator_test.dart index c2a1ea8..3b177a2 100644 --- a/test/domain/irr_calculator_test.dart +++ b/test/domain/irr_calculator_test.dart @@ -1,35 +1,21 @@ import 'package:test/test.dart'; import 'package:wealth_wave/domain/models/irr_calculator.dart'; -import 'package:wealth_wave/domain/models/transaction.dart'; +import 'package:wealth_wave/domain/models/payment.dart'; void main() { group('IRRCalculator', () { test('calculateIRR returns correct IRR', () { final calculator = IRRCalculator(); final transactions = [ - Transaction( - id: 0, - description: null, - amount: 1000.0, - createdOn: DateTime(2020, 1, 1)), - Transaction( - id: 0, - description: null, - amount: 2000.0, - createdOn: DateTime(2021, 1, 1)), - Transaction( - id: 0, - description: null, - amount: 3000.0, - createdOn: DateTime(2022, 1, 1)), + Payment(amount: 1000.0, createdOn: DateTime(2020, 1, 1)), + Payment(amount: 2000.0, createdOn: DateTime(2021, 1, 1)), + Payment(amount: 3000.0, createdOn: DateTime(2022, 1, 1)), ]; const finalValue = 8000.0; final finalDate = DateTime(2023, 1, 1); final irr = calculator.calculateIRR( - payments: transactions, - value: finalValue, - valueUpdatedOn: finalDate); + payments: transactions, value: finalValue, valueUpdatedOn: finalDate); expect(irr, closeTo(0.2, 0.02)); // Expected IRR is 20%, tolerance is 1% });