Skip to content

Commit 7876cae

Browse files
authored
feat: simplified goal check (#64)
* feat: simplified goal check * docs: updated usage BREAKING CHANGE: this alters core flows
1 parent 9232f7c commit 7876cae

14 files changed

+97
-380
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,4 @@ The example result object models:
258258

259259
```
260260

261-
In it you can get an estimate monthly cash flow by calling `const estimated = timeline.getEstimatedMonthlyCashFlow();`
261+
In it you can get an estimate monthly cash flow by calling `const mostRecentMonthlyCashflow = timeline.getCashFlowMonthByEndDate();`

src/account/i-user-goal.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { IRentalPropertyEntity } from '../properties/i-rental-property-entity';
2-
31
export interface IUserGoal {
42
/**
53
* used to determine how much you need want for monthly expenses
@@ -9,7 +7,6 @@ export interface IUserGoal {
97
/**
108
* method used to help determine if you have met your expenses
119
* @param today
12-
* @param properties
1310
*/
14-
metMonthlyGoal(today: Date, properties: IRentalPropertyEntity[]): boolean;
11+
metMonthlyGoal(today: Date): boolean;
1512
}

src/account/user.ts

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ export interface IUser extends IUserInvestorCheck {
2222
/**
2323
* method used to help determine if you have met your expenses
2424
* @param today
25-
* @param properties
2625
*/
27-
getEstimatedMonthlyCashFlow(today: Date, properties: IRentalPropertyEntity[]): number;
26+
getCashFlowMonth(today: Date): number;
2827

2928
clone(): IUser;
3029
}
@@ -48,23 +47,17 @@ export class User implements IUser {
4847
/**
4948
* method used to help determine if you have met your expenses
5049
* @param today
51-
* @param properties
5250
*/
53-
metMonthlyGoal(today: Date, properties: IRentalPropertyEntity[]): boolean {
54-
return this.getEstimatedMonthlyCashFlow(today, properties) >= this.monthlyIncomeAmountGoal;
51+
metMonthlyGoal(today: Date): boolean {
52+
return this.getCashFlowMonth(today) >= this.monthlyIncomeAmountGoal;
5553
}
5654

57-
getEstimatedMonthlyCashFlow(today: Date, properties: IRentalPropertyEntity[]): number {
58-
if (!properties || properties.length === 0) {
55+
getCashFlowMonth(today: Date): number {
56+
if (!this.ledgerCollection) {
5957
return 0;
6058
}
6159

62-
return currency(
63-
properties.reduce(
64-
(previousValue, currentValue) => previousValue + currentValue.getEstimatedMonthlyCashFlow(today),
65-
0
66-
)
67-
);
60+
return currency(this.ledgerCollection.getCashFlowMonth(today));
6861
}
6962

7063
/**
@@ -120,8 +113,7 @@ export class User implements IUser {
120113
let minMonthsRequired = 0;
121114
if (this.loanSettings && this.loanSettings.length > 0) {
122115
const found = this.loanSettings.find(
123-
(ls) =>
124-
ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily
116+
(ls) => ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily,
125117
);
126118
minMonthsRequired = found?.value ?? 0;
127119
}
@@ -138,8 +130,7 @@ export class User implements IUser {
138130
let minMonthsRequired = 0;
139131
if (this.loanSettings && this.loanSettings.length > 0) {
140132
const found = this.loanSettings.find(
141-
(ls) =>
142-
ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily
133+
(ls) => ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily,
143134
);
144135
minMonthsRequired = found?.value ?? 0;
145136
}
@@ -156,16 +147,12 @@ export class User implements IUser {
156147
let minMonthsRequired = 0;
157148
if (this.loanSettings && this.loanSettings.length > 0) {
158149
const found = this.loanSettings.find(
159-
(ls) =>
160-
ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily
150+
(ls) => ls.name === LoanSettings.MinimumMonthlyReservesForRental && ls.propertyType === PropertyType.SingleFamily,
161151
);
162152
minMonthsRequired = found?.value ?? 0;
163153
}
164154

165-
return (
166-
this.ledgerCollection.getBalance(date) -
167-
this.ledgerCollection.getMinimumSavings(properties, date, minMonthsRequired)
168-
);
155+
return this.ledgerCollection.getBalance(date) - this.ledgerCollection.getMinimumSavings(properties, date, minMonthsRequired);
169156
}
170157

171158
clone(): IUser {

src/ledger/ledger-collection.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,7 @@ export class LedgerCollection implements ILedgerCollection {
8787
return 0;
8888
}
8989

90-
return collection
91-
.filter((x) => x.typeMatches(type))
92-
.reduce((previousValue, currentValue) => previousValue + currentValue.amount, 0);
90+
return collection.filter((x) => x.typeMatches(type)).reduce((previousValue, currentValue) => previousValue + currentValue.amount, 0);
9391
}
9492

9593
constructor() {
@@ -118,7 +116,7 @@ export class LedgerCollection implements ILedgerCollection {
118116
}
119117
return this.filter((i) => (date ? i.created.getTime() <= date.getTime() : !!i)).reduce(
120118
(previousValue, currentValue) => previousValue + currentValue.amount,
121-
0
119+
0,
122120
);
123121
}
124122

@@ -154,7 +152,7 @@ export class LedgerCollection implements ILedgerCollection {
154152

155153
return (
156154
itiriri(properties.filter((p) => p.propertyType === PropertyType.SingleFamily)).sum((r) =>
157-
r.getExpensesByDate(date ?? this.collection.last().created)
155+
r.getExpensesByDate(date ?? this.collection.last().created),
158156
) * minMonthsRequired || 0
159157
);
160158
}
@@ -257,15 +255,10 @@ export class LedgerCollection implements ILedgerCollection {
257255
const ledgerItemsCashFlow = boundary.filter((x) => x.type === LedgerItemType.CashFlow);
258256

259257
result.cashFlow = this.getSummaryByType(boundary, LedgerItemType.CashFlow);
260-
result.averageCashFlow = currency(
261-
ledgerItemsCashFlow.length > 0 ? result.cashFlow / ledgerItemsCashFlow.length : 0
262-
);
258+
result.averageCashFlow = currency(ledgerItemsCashFlow.length > 0 ? result.cashFlow / ledgerItemsCashFlow.length : 0);
263259
result.equity = this.getSummaryByType(boundary, LedgerItemType.Equity);
264260
result.purchases = this.getSummaryByType(boundary, LedgerItemType.Purchase);
265-
result.balance = this.filter((li) => li.dateNotGreaterThan(date)).reduce(
266-
(previousValue, currentValue) => previousValue + currentValue.amount,
267-
0
268-
);
261+
result.balance = this.filter((li) => li.dateNotGreaterThan(date)).reduce((previousValue, currentValue) => previousValue + currentValue.amount, 0);
269262

270263
return result;
271264
}
@@ -321,10 +314,7 @@ export class LedgerCollection implements ILedgerCollection {
321314
} else {
322315
for (let month = boundary[0].created.getUTCMonth(); month < 12; month++) {
323316
const expectedDate = new Date(Date.UTC(year, month, 1));
324-
if (
325-
!boundary.some((x) => x.dateMatchesYearAndMonth(expectedDate)) &&
326-
expectedDate.getTime() >= lastLedgerItem.created.getTime()
327-
) {
317+
if (!boundary.some((x) => x.dateMatchesYearAndMonth(expectedDate)) && expectedDate.getTime() >= lastLedgerItem.created.getTime()) {
328318
const summaryMonth = this.getSummaryMonth(lastLedgerItem.created);
329319
summaryMonth.date = cloneDateUtc(expectedDate);
330320
collection.push(summaryMonth);

src/time/default-has-met-goal-or-max-time.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
11
import { HasMetGoalOrMaxTime } from './has-met-goal-or-max-time';
22
import { isEqual, differenceInMonths } from 'date-fns';
33
import { IUser } from '../account/user';
4-
import { IRentalPropertyEntity } from '../properties/i-rental-property-entity';
54
import { cloneDateUtc } from '../utils/data-clone-date';
65

76
/**
87
* Method will compare basic dates w/ null user and all rentals
98
* @param start
109
* @param today
1110
* @param user
12-
* @param rentals
1311
* @param maxYears
1412
*/
15-
export const defaultHasMetGoalOrMaxTime: HasMetGoalOrMaxTime = (
16-
start: Date,
17-
today: Date,
18-
user: IUser,
19-
rentals: IRentalPropertyEntity[],
20-
maxYears: number
21-
): boolean => {
13+
export const defaultHasMetGoalOrMaxTime: HasMetGoalOrMaxTime = (start: Date, today: Date, user: IUser, maxYears: number): boolean => {
2214
if (isEqual(start, today) && !user) {
2315
return false;
2416
}
2517

26-
if (user && user.metMonthlyGoal(today, rentals)) {
18+
if (user && user.metMonthlyGoal(today)) {
2719
return true;
2820
}
2921

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { IUser } from '../account/user';
2-
import { IRentalPropertyEntity } from '../properties/i-rental-property-entity';
32

43
/**
54
* a way to determine if the loop should end based on the user's rules
65
*/
7-
export type HasMetGoalOrMaxTime = (
8-
start: Date,
9-
today: Date,
10-
user: IUser,
11-
rentals: IRentalPropertyEntity[],
12-
maxYears: number
13-
) => boolean;
6+
export type HasMetGoalOrMaxTime = (start: Date, today: Date, user: IUser, maxYears: number) => boolean;

src/time/movement.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { looper } from './looper';
1313

1414
/**
1515
* This is where the magic happens. You provide the options, and you let this calculate the rest.
16+
* The flow is that you do work on a day, then after the changes are done for that day you evaluate it to determine if you met the goal or reached the end.
17+
* If you did not meet the goal you start a new day and try again.
1618
* @param options
1719
* @param user
1820
*/
@@ -31,9 +33,7 @@ export function movement(options: ILoopOptions, user: IUser): ITimeline {
3133
}
3234

3335
if (!options.propertyGeneratorPassiveApartment && !options.propertyGeneratorSingleFamily) {
34-
throw new Error(
35-
'Invalid Argument: must declare at least 1, either propertyGeneratorSingleFamily or propertyGeneratorPassiveApartment'
36-
);
36+
throw new Error('Invalid Argument: must declare at least 1, either propertyGeneratorSingleFamily or propertyGeneratorPassiveApartment');
3737
}
3838

3939
ensureArray<ILoanSetting>(user.loanSettings, {
@@ -51,12 +51,7 @@ export function movement(options: ILoopOptions, user: IUser): ITimeline {
5151
ignoreError: !options.propertyGeneratorSingleFamily && !!options.propertyGeneratorPassiveApartment,
5252
});
5353

54-
let result: ITimeline = new Timeline(
55-
cloneDateUtc(options.startDate),
56-
cloneDateUtc(options.startDate),
57-
[],
58-
user.clone()
59-
);
54+
let result: ITimeline = new Timeline(cloneDateUtc(options.startDate), cloneDateUtc(options.startDate), [], user.clone());
6055

6156
do {
6257
result = looper(options, result);
@@ -65,8 +60,7 @@ export function movement(options: ILoopOptions, user: IUser): ITimeline {
6560
result.startDate,
6661
cloneDateUtc(result.user.ledgerCollection.getLatestLedgerItem().created),
6762
result.user,
68-
result.rentals.map((x) => x.property).filter((x) => x.isOwned),
69-
options.maxYears
63+
options.maxYears,
7064
)
7165
);
7266

src/time/timeline.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { IHistoricalProperty } from './i-historical-property';
22
import { IUser } from '../account/user';
3-
import { ILedgerSummary } from '../ledger/i-ledger-summary';
43
import { cloneDateUtc } from '../utils';
54

65
export interface ITimeline {
@@ -9,7 +8,7 @@ export interface ITimeline {
98
rentals: IHistoricalProperty[];
109
user: IUser;
1110

12-
getEstimatedMonthlyCashFlow(): number;
11+
getCashFlowMonthByEndDate(): number;
1312

1413
getBalance(date?: Date): number;
1514

@@ -29,35 +28,14 @@ export class Timeline implements ITimeline {
2928
startDate: Date;
3029
user: IUser;
3130

32-
getEstimatedMonthlyCashFlow(): number {
33-
return this.user.getEstimatedMonthlyCashFlow(
34-
this.endDate,
35-
this.rentals.map((x) => x.property).filter((x) => x.isOwned)
36-
);
31+
getCashFlowMonthByEndDate(): number {
32+
return this.user.getCashFlowMonth(cloneDateUtc(this.endDate));
3733
}
3834

3935
getBalance(date?: Date): number {
4036
return this.user.ledgerCollection.getBalance(date ?? this.endDate);
4137
}
4238

43-
getSummariesAnnualByYear(year?: number): ILedgerSummary[] {
44-
return this.user.ledgerCollection.getSummariesAnnual(year ?? this.endDate.getUTCFullYear());
45-
}
46-
47-
getAllSummariesAnnual(): ILedgerSummary[] {
48-
let result: ILedgerSummary[] = [];
49-
50-
for (
51-
let startYear: number = this.startDate.getUTCFullYear();
52-
startYear < this.endDate.getUTCFullYear();
53-
startYear++
54-
) {
55-
result = result.concat(this.getSummariesAnnualByYear(startYear));
56-
}
57-
58-
return result;
59-
}
60-
6139
clone(): ITimeline {
6240
return new Timeline(
6341
cloneDateUtc(this.startDate),
@@ -66,7 +44,7 @@ export class Timeline implements ITimeline {
6644
property: x.property.clone(),
6745
reasons: x.reasons.map((x) => ({ ...x })),
6846
})),
69-
this.user.clone()
47+
this.user.clone(),
7048
);
7149
}
7250
}

tests/accounts/user.spec.ts

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -160,56 +160,17 @@ describe('User unit tests', () => {
160160
});
161161

162162
describe('and metMonthlyGoal', () => {
163-
let rental: jest.Mocked<IRentalPropertyEntity>;
164-
beforeEach(() => {
165-
rental = {
166-
sellPriceByDate: jest.fn(),
167-
getEstimatedEquityFromSell: jest.fn(),
168-
estimatedCashOnCashReturn: 0,
169-
estimatedReturnOnCapitalGain: 0,
170-
isAvailable: false,
171-
wasPurchased: true,
172-
rawEstimatedAnnualCashFlow: 0,
173-
getExpensesByDate: jest.fn(),
174-
getEstimatedMonthlyCashFlow: jest.fn(),
175-
offeredInvestmentAmounts: [0],
176-
propertyType: PropertyType.SingleFamily,
177-
clone: jest.fn().mockReturnThis(),
178-
equityCapturePercent: 0,
179-
minSellYears: 0,
180-
rawCashFlow: 0,
181-
address: '',
182-
availableEndDate: new Date(),
183-
availableStartDate: new Date(),
184-
id: '',
185-
isOwned: false,
186-
purchaseDate: undefined,
187-
purchasePrice: 0,
188-
sellPriceAppreciationPercent: 0,
189-
soldDate: undefined,
190-
canInvestByUser: jest.fn(),
191-
canSell: jest.fn(),
192-
get costDownPrice(): number {
193-
return 0;
194-
},
195-
getEquityFromSell: jest.fn(),
196-
getCashFlowByDate: jest.fn(),
197-
isAvailableByDate: jest.fn(),
198-
};
199-
});
200-
201163
test('should be true', () => {
202164
const date = new Date();
203165

204166
const expected = 500;
205167

206-
rental.getEstimatedMonthlyCashFlow.mockReturnValue(expected);
207-
208168
instance.monthlyIncomeAmountGoal = 500;
209169

210-
expect(instance.metMonthlyGoal(date, [rental])).toBeTruthy();
211-
expect(instance.getEstimatedMonthlyCashFlow(date, [rental])).toEqual(expected);
212-
expect(rental.getEstimatedMonthlyCashFlow).toHaveBeenCalledWith(date);
170+
ledgerCollection.getCashFlowMonth.mockReturnValue(expected);
171+
172+
expect(instance.metMonthlyGoal(date)).toBeTruthy();
173+
expect(instance.getCashFlowMonth(date)).toEqual(expected);
213174
});
214175

215176
test('should be false', () => {
@@ -219,11 +180,10 @@ describe('User unit tests', () => {
219180

220181
instance.monthlyIncomeAmountGoal = 500;
221182

222-
rental.getEstimatedMonthlyCashFlow.mockReturnValue(expected);
183+
ledgerCollection.getCashFlowMonth.mockReturnValue(expected);
223184

224-
expect(instance.metMonthlyGoal(date, [rental])).toBeFalsy();
225-
expect(instance.getEstimatedMonthlyCashFlow(date, [rental])).toEqual(expected);
226-
expect(rental.getEstimatedMonthlyCashFlow).toHaveBeenCalledWith(date);
185+
expect(instance.metMonthlyGoal(date)).toBeFalsy();
186+
expect(instance.getCashFlowMonth(date)).toEqual(expected);
227187
});
228188
});
229189
});

0 commit comments

Comments
 (0)