Skip to content

Commit

Permalink
save commit
Browse files Browse the repository at this point in the history
  • Loading branch information
praslnx8 committed Jan 18, 2024
1 parent afb0613 commit 5889cd6
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 62 deletions.
21 changes: 21 additions & 0 deletions lib/domain/models/goal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,27 @@ class Goal {
.then((count) => Void);
}

Future<double> 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,
Expand Down
22 changes: 19 additions & 3 deletions lib/domain/models/investment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -229,6 +226,25 @@ class Investment {
.then((count) => Void);
}

Future<double> getIRR() async {
if (irr != null) {
return Future(() => irr!);
} else if (value != null && valueUpdatedOn != null) {
List<Payment> 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,
Expand Down
6 changes: 3 additions & 3 deletions lib/domain/models/irr_calculator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import 'dart:math';
import 'package:wealth_wave/domain/models/payment.dart';

class IRRCalculator {
double? calculateIRR(
double calculateIRR(
{required final List<Payment> 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;
Expand All @@ -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(
Expand Down
50 changes: 33 additions & 17 deletions lib/presentation/goals_presenter.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,23 +14,10 @@ class GoalsPresenter extends Presenter<GoalsViewState> {
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}) {
Expand All @@ -38,26 +26,54 @@ class GoalsPresenter extends Presenter<GoalsViewState> {
}

class GoalsViewState {
List<GoalVO> goals = [];
List<GoalVO> goalVOs = [];
}

class GoalVO {
final int id;
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<Investment, double> investments;

double get yearsLeft => DateTime.now().difference(maturityDate).inDays / 365;

double get progress => valueOnMaturity / maturityAmount;

GoalVO(
{required this.id,
required this.name,
required this.description,
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<GoalVO> 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());
}
}
40 changes: 20 additions & 20 deletions lib/ui/pages/goals_page.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,14 +23,14 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {

@override
Widget buildWidget(BuildContext context, GoalsViewState snapshot) {
List<Goal> goals = snapshot.goals;
List<GoalVO> 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(
Expand All @@ -51,7 +50,7 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
}

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(
Expand All @@ -64,21 +63,22 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
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<int>(
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) => [
Expand All @@ -97,11 +97,11 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
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'),
),
],
),
Expand All @@ -111,7 +111,7 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
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),
Expand All @@ -121,10 +121,10 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
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),
],
),
Expand All @@ -133,17 +133,17 @@ class _GoalsPage extends PageState<GoalsViewState, GoalsPage, GoalsPresenter> {
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),
Expand Down
24 changes: 5 additions & 19 deletions test/domain/irr_calculator_test.dart
Original file line number Diff line number Diff line change
@@ -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%
});
Expand Down

0 comments on commit 5889cd6

Please sign in to comment.