Skip to content

Commit 7c08707

Browse files
committed
feat: created ledger
1 parent 037664a commit 7c08707

29 files changed

+1057
-143
lines changed

lint-staged.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
'*.ts': [
33
'eslint --config .eslintrc.json --cache --fix',
4-
'npm run test:related -- '
4+
'jest --findRelatedTests'
55
],
66
'*.{ts,css,md}': 'prettier --write'
77
};

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"dependencies": {
6262
"chance": "^1.1.8",
6363
"date-fns": "^2.24.0",
64+
"itiriri": "^2.0.1",
6465
"lodash.clonedeep": "^4.5.0"
6566
},
6667
"publishConfig": {

src/account/i-user.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { ILoanSetting } from './i-loan-settings';
22
import { IUserGoal } from './i-user-goal';
3+
import { ILedgerCollection } from '../ledger/ledger-collection';
34

45
export interface IUser {
56
loanSettings: ILoanSetting[];
7+
68
goals: IUserGoal;
9+
10+
monthlySavedAmount: number;
11+
12+
ledger: ILedgerCollection;
13+
14+
getLedgerBalance(): number;
15+
16+
hasMoneyToInvest(): boolean;
717
}

src/goals/get-rules.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/goals/goal-rule.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/goals/i-goal-rule-evaluation.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/goals/i-goal-rule.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/ledger/i-ledger-summary.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface ILedgerSummary {
2+
date: Date;
3+
balance: number;
4+
cashFlow: number;
5+
averageCashFlow: number;
6+
purchases: number;
7+
equity: number;
8+
}

src/ledger/ledger-collection.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { LedgerItem } from './ledger-item';
2+
import itiriri, { IterableQuery } from 'itiriri';
3+
import { ILedgerSummary } from './i-ledger-summary';
4+
import { LedgerItemType } from './ledger-item-type';
5+
import { IPropertyEntity, RentalSingleFamily } from '../properties';
6+
7+
export interface ILedgerCollection {
8+
getBalance(): number;
9+
10+
add(item: LedgerItem | Iterable<LedgerItem>): void;
11+
12+
getCashFlowMonth(date: Date): number;
13+
14+
getSummaryMonth(date: Date): ILedgerSummary;
15+
16+
getSummaryAnnual(year: number): ILedgerSummary;
17+
18+
getSummariesAnnual(year: number): ILedgerSummary[];
19+
}
20+
21+
export class LedgerCollection implements ILedgerCollection {
22+
private collection: IterableQuery<LedgerItem>;
23+
24+
private getSummaryByType(collection: IterableQuery<LedgerItem>, type: LedgerItemType): number {
25+
if (!collection) {
26+
return 0;
27+
}
28+
return collection.filter((x) => x.typeMatches(type)).sum((x) => x.amount) || 0;
29+
}
30+
31+
constructor() {
32+
this.collection = itiriri([]);
33+
}
34+
35+
getBalance(): number {
36+
return this.isEmpty() ? 0 : this.collection.sum((x) => x.amount);
37+
}
38+
39+
add(item: LedgerItem | Iterable<LedgerItem>): void {
40+
this.collection = itiriri(this.collection.prepend(item).toArray());
41+
}
42+
43+
first?(): LedgerItem {
44+
if (this.isEmpty()) {
45+
return null;
46+
}
47+
48+
return this.collection.first();
49+
}
50+
51+
last?(): LedgerItem {
52+
if (this.isEmpty()) {
53+
return null;
54+
}
55+
56+
return this.collection.last();
57+
}
58+
59+
isEmpty(): boolean {
60+
return this.collection.length() === 0;
61+
}
62+
63+
getMinimumSavings(date: Date, properties: IPropertyEntity[], minMonthsRequired = 6): number {
64+
if (!date) {
65+
throw new Error('no date supplied');
66+
}
67+
68+
if (!properties || !properties.length) {
69+
return 0;
70+
}
71+
72+
return (
73+
itiriri(properties.filter((p) => p instanceof RentalSingleFamily)).sum((r) =>
74+
(<RentalSingleFamily>r).getMonthlyPrincipalInterestTaxInterestByDate(date)
75+
) * minMonthsRequired
76+
);
77+
}
78+
79+
hasMinimumSavings(date: Date, properties: IPropertyEntity[], minMonthsRequired = 6): boolean {
80+
return this.getBalance() >= this.getMinimumSavings(date, properties, minMonthsRequired);
81+
}
82+
83+
getCashFlowMonth(date: Date): number {
84+
if (!date) {
85+
throw new Error('no date supplied');
86+
}
87+
88+
if (this.isEmpty()) {
89+
return 0;
90+
}
91+
92+
const boundary = this.collection.filter((li) => li.dateMatchesYearAndMonth(date));
93+
94+
if (!boundary) {
95+
return 0;
96+
}
97+
98+
return this.getSummaryByType(boundary, LedgerItemType.CashFlow);
99+
}
100+
101+
getSummaryMonth(date: Date): ILedgerSummary {
102+
if (!date) {
103+
throw new Error('no date supplied');
104+
}
105+
106+
const result: ILedgerSummary = {
107+
date: new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)),
108+
balance: 0,
109+
cashFlow: 0,
110+
averageCashFlow: 0,
111+
equity: 0,
112+
purchases: 0,
113+
};
114+
115+
if (this.isEmpty()) {
116+
return result;
117+
}
118+
119+
const boundary = this.collection.filter((li) => li.dateMatchesYearAndMonth(date));
120+
121+
if (!boundary) {
122+
return result;
123+
}
124+
125+
const salary = this.getSummaryByType(boundary, LedgerItemType.Salary);
126+
result.cashFlow = this.getSummaryByType(boundary, LedgerItemType.CashFlow);
127+
result.averageCashFlow = boundary.filter((x) => x.type === LedgerItemType.CashFlow).average((x) => x.amount) || 0;
128+
result.equity = this.getSummaryByType(boundary, LedgerItemType.Equity);
129+
result.purchases = this.getSummaryByType(boundary, LedgerItemType.Purchase);
130+
result.balance = result.cashFlow + salary + result.equity - result.purchases || 0;
131+
132+
return result;
133+
}
134+
135+
getSummaryAnnual(year: number): ILedgerSummary {
136+
const summaries = itiriri(this.getSummariesAnnual(year));
137+
138+
return {
139+
date: summaries.first().date,
140+
balance: summaries.sum((x) => x.balance || 0),
141+
equity: summaries.sum((x) => x.equity || 0),
142+
cashFlow: summaries.sum((x) => x.cashFlow || 0),
143+
averageCashFlow: summaries.average((x) => x.cashFlow || 0),
144+
purchases: summaries.average((x) => x.purchases || 0),
145+
};
146+
}
147+
148+
getSummariesAnnual(year: number): ILedgerSummary[] {
149+
if (!year) {
150+
throw new Error('year is missing');
151+
}
152+
153+
if (this.isEmpty()) {
154+
return [];
155+
}
156+
157+
const boundary = this.collection.filter((li) => li.dateMatchesYear(year));
158+
159+
if (!boundary) {
160+
return [];
161+
}
162+
163+
const collection = [];
164+
for (let month = boundary.first().created.getUTCMonth(); month < 12; month++) {
165+
collection.push(this.getSummaryMonth(new Date(Date.UTC(year, month, 1))));
166+
}
167+
168+
return collection;
169+
}
170+
}

0 commit comments

Comments
 (0)