Skip to content

Commit 4691bc3

Browse files
authored
feat: check met goal in loop (#67)
1 parent f36f4a0 commit 4691bc3

File tree

2 files changed

+147
-80
lines changed

2 files changed

+147
-80
lines changed

src/time/looper.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ export const looper: LooperType = (options: ILoopRecursiveOptions, timeline: ITi
3030
options.propertyGeneratorSingleFamily,
3131
result.rentals,
3232
timeline.endDate,
33-
result.user.loanSettings
33+
result.user.loanSettings,
3434
);
3535

3636
result.rentals = updateHistoricalRentals(
3737
RentalPassiveApartment,
3838
options.propertyGeneratorPassiveApartment,
3939
result.rentals,
4040
timeline.endDate,
41-
result.user.loanSettings
41+
result.user.loanSettings,
4242
);
4343

4444
//step 2: get cash flow
@@ -60,6 +60,10 @@ export const looper: LooperType = (options: ILoopRecursiveOptions, timeline: ITi
6060
}
6161
}
6262

63+
if (result.user.metMonthlyGoal(result.endDate)) {
64+
return result;
65+
}
66+
6367
//step 3: sell properties
6468
const forSaleProperties = result.rentals
6569
.filter((r) => r.property && r.property.canSell(timeline.endDate))
@@ -76,16 +80,14 @@ export const looper: LooperType = (options: ILoopRecursiveOptions, timeline: ITi
7680
date.setUTCHours(3);
7781
date.setUTCMinutes(index);
7882
});
79-
equityFromSell.note = `for: ${pr.property.address}, id: ${pr.property.id} (${
80-
PropertyType[pr.property.propertyType]
81-
})`;
83+
equityFromSell.note = `for: ${pr.property.address}, id: ${pr.property.id} (${PropertyType[pr.property.propertyType]})`;
8284
result.user.ledgerCollection.add(equityFromSell);
8385
}
8486

8587
if (
8688
!result.user.hasMoneyToInvest(
8789
timeline.endDate,
88-
result.rentals.map((x) => x.property).filter((x) => x.isOwned)
90+
result.rentals.map((x) => x.property).filter((x) => x.isOwned),
8991
)
9092
) {
9193
return result;
@@ -98,15 +100,15 @@ export const looper: LooperType = (options: ILoopRecursiveOptions, timeline: ITi
98100
const validator = r.property.canInvestByUser(
99101
result.user,
100102
timeline.endDate,
101-
result.rentals.map((h) => h.property)
103+
result.rentals.map((h) => h.property),
102104
);
103105

104106
if (!validator.canInvest) {
105107
r.reasons = r.reasons.concat(
106108
validator.results.map((reasons) => ({
107109
reason: reasons.message,
108110
date: cloneDateUtc(timeline.endDate),
109-
}))
111+
})),
110112
);
111113
}
112114

@@ -133,17 +135,15 @@ export const looper: LooperType = (options: ILoopRecursiveOptions, timeline: ITi
133135
result.user.hasMoneyToInvest(
134136
purchaseCreated,
135137
result.rentals.map((x) => x.property),
136-
minCostDownByRule
138+
minCostDownByRule,
137139
)
138140
) {
139141
// buy
140142
const purchase = new LedgerItem();
141143
purchase.amount = minCostDownByRule * -1;
142144
purchase.type = LedgerItemType.Purchase;
143145
purchase.created = purchaseCreated;
144-
purchase.note = `for: ${rentalProperty.address}, id: ${rentalProperty.id} (${
145-
PropertyType[rentalProperty.propertyType]
146-
})`;
146+
purchase.note = `for: ${rentalProperty.address}, id: ${rentalProperty.id} (${PropertyType[rentalProperty.propertyType]})`;
147147

148148
if (!purchase.isAmountGreaterThanZero()) {
149149
result.user.ledgerCollection.add(purchase);

tests/time/looper.spec.ts

Lines changed: 135 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ describe('looper unit tests', () => {
115115
describe('and monthlySavedAmount', () => {
116116
beforeEach(async () => {
117117
updateHistoricalRentals.mockReturnValue([]);
118-
const movement = (await import('../../src/time/looper')).looper;
119-
movement(
118+
const looper = (await import('../../src/time/looper')).looper;
119+
looper(
120120
{
121121
propertyGeneratorSingleFamily: rentalGeneratorHome,
122122
propertyGeneratorPassiveApartment: rentalGeneratorPassive,
@@ -148,7 +148,7 @@ describe('looper unit tests', () => {
148148

149149
describe('and hasMoneyToInvest is false', () => {
150150
beforeEach(async () => {
151-
user.hasMoneyToInvest.mockReturnValue(false);
151+
user.hasMoneyToInvest.mockReturnValueOnce(false);
152152
updateHistoricalRentals.mockReturnValue([]);
153153
const looper = (await import('../../src/time/looper')).looper;
154154
looper(
@@ -185,7 +185,7 @@ describe('looper unit tests', () => {
185185
let actual: ITimeline;
186186

187187
beforeEach(async () => {
188-
user.hasMoneyToInvest.mockReturnValue(true);
188+
user.hasMoneyToInvest.mockReturnValueOnce(true);
189189
updateHistoricalRentals.mockReturnValue([]);
190190
const looper = (await import('../../src/time/looper')).looper;
191191
actual = looper(
@@ -230,6 +230,7 @@ describe('looper unit tests', () => {
230230
describe('and properties', () => {
231231
let actual: ITimeline;
232232
let rentalSF: jest.Mocked<RentalSingleFamily>;
233+
let rentalSFToPurchase: jest.Mocked<RentalSingleFamily>;
233234
let rentalPA: jest.Mocked<RentalPassiveApartment>;
234235

235236
afterEach(() => {
@@ -247,87 +248,151 @@ describe('looper unit tests', () => {
247248
value: PropertyType.SingleFamily,
248249
});
249250

251+
rentalSFToPurchase = new RentalSingleFamily() as jest.Mocked<RentalSingleFamily>;
252+
rentalSFToPurchase.address = 'sf addy pur';
253+
rentalSFToPurchase.id = 'sf-id-2';
254+
rentalSFToPurchase.canSell.mockReturnValueOnce(false);
255+
rentalSFToPurchase.canInvestByUser.mockReturnValue({ canInvest: true, results: [] });
256+
Object.defineProperty(rentalSFToPurchase, 'propertyType', {
257+
value: PropertyType.SingleFamily,
258+
});
259+
250260
rentalPA = new RentalPassiveApartment() as jest.Mocked<RentalPassiveApartment>;
251261
rentalPA.address = 'pa addy';
252262
rentalPA.id = 'pa-id';
253263
Object.defineProperty(rentalPA, 'propertyType', {
254264
value: PropertyType.PassiveApartment,
255265
});
256266

257-
updateHistoricalRentals.mockReturnValue([rentalSF, rentalPA].map((x) => ({ property: x, reasons: [] })));
267+
updateHistoricalRentals.mockReturnValue([rentalSF, rentalSFToPurchase, rentalPA].map((x) => ({ property: x, reasons: [] })));
258268
});
259269

260270
describe('and cash flow', () => {
261-
beforeEach(async () => {
262-
Object.defineProperty(rentalSF, 'isOwned', {
263-
get: jest.fn().mockReturnValue(true),
264-
});
265-
//rentalSF.isOwned = true;
266-
rentalSF.getCashFlowByDate.mockReturnValue(7);
271+
describe('and has not met goal', () => {
272+
beforeEach(() => {
273+
Object.defineProperty(rentalSF, 'isOwned', {
274+
get: jest.fn().mockReturnValueOnce(true),
275+
});
267276

268-
Object.defineProperty(rentalPA, 'isOwned', {
269-
get: jest.fn().mockReturnValue(true),
277+
//rentalSF.isOwned = true;
278+
rentalSF.canSell.mockReturnValue(true);
279+
rentalSFToPurchase.canSell.mockReturnValue(false);
280+
281+
rentalSF.getCashFlowByDate.mockReturnValue(7);
282+
rentalSFToPurchase.getCashFlowByDate.mockReturnValue(0);
283+
284+
Object.defineProperty(rentalPA, 'isOwned', {
285+
get: jest.fn().mockReturnValueOnce(true),
286+
});
287+
288+
//rentalPA.isOwned = true;
289+
rentalPA.getCashFlowByDate.mockReturnValue(10);
290+
291+
user.monthlySavedAmount = 0;
270292
});
271-
//rentalPA.isOwned = true;
272-
rentalPA.getCashFlowByDate.mockReturnValue(10);
273293

274-
user.monthlySavedAmount = 0;
294+
test('should add to ledger', async () => {
295+
const looper = (await import('../../src/time/looper')).looper;
296+
actual = looper(
297+
{
298+
propertyGeneratorSingleFamily: rentalGeneratorHome,
299+
propertyGeneratorPassiveApartment: rentalGeneratorPassive,
300+
},
301+
{
302+
user,
303+
endDate: new Date(),
304+
getBalance: jest.fn(),
305+
startDate: new Date(),
306+
rentals: [],
307+
clone: jest.fn().mockReturnThis(),
308+
getCashFlowMonthByEndDate: jest.fn(),
309+
},
310+
);
275311

276-
const looper = (await import('../../src/time/looper')).looper;
277-
actual = looper(
278-
{
279-
propertyGeneratorSingleFamily: rentalGeneratorHome,
280-
propertyGeneratorPassiveApartment: rentalGeneratorPassive,
281-
},
282-
{
283-
user,
284-
endDate: new Date(),
285-
getBalance: jest.fn(),
286-
startDate: new Date(),
287-
rentals: [],
288-
clone: jest.fn().mockReturnThis(),
289-
getCashFlowMonthByEndDate: jest.fn(),
290-
},
291-
);
312+
expect(actual.user.ledgerCollection.add).toHaveBeenCalledTimes(3);
313+
314+
expect(actual.user.ledgerCollection.add).toHaveBeenNthCalledWith(
315+
1,
316+
expect.objectContaining({
317+
amount: rentalSF.getCashFlowByDate(null),
318+
type: LedgerItemType.CashFlow,
319+
created: expectedToday,
320+
note: `for: ${rentalSF.address}, id: ${rentalSF.id} (${PropertyType[rentalSF.propertyType]})`,
321+
}),
322+
);
323+
324+
expect(actual.user.ledgerCollection.add).toHaveBeenNthCalledWith(
325+
2,
326+
expect.objectContaining({
327+
amount: rentalPA.getCashFlowByDate(null),
328+
type: LedgerItemType.CashFlow,
329+
created: expectedToday,
330+
note: `for: ${rentalPA.address}, id: ${rentalPA.id} (${PropertyType[rentalPA.propertyType]})`,
331+
}),
332+
);
333+
});
292334
});
293335

294-
test('should add to ledger', () => {
295-
expect(actual.user.ledgerCollection.add).toHaveBeenCalledTimes(2);
336+
describe('and meets goal after cash flow', () => {
337+
beforeEach(() => {
338+
Object.defineProperty(rentalSF, 'isOwned', {
339+
get: jest.fn().mockReturnValueOnce(true),
340+
});
296341

297-
expect(actual.user.ledgerCollection.add).toHaveBeenNthCalledWith(
298-
1,
299-
expect.objectContaining({
300-
amount: rentalSF.getCashFlowByDate(null),
301-
type: LedgerItemType.CashFlow,
302-
created: expectedToday,
303-
note: `for: ${rentalSF.address}, id: ${rentalSF.id} (${PropertyType[rentalSF.propertyType]})`,
304-
}),
305-
);
342+
//rentalSF.isOwned = true;
343+
rentalSF.getCashFlowByDate.mockReturnValueOnce(7);
306344

307-
expect(actual.user.ledgerCollection.add).toHaveBeenNthCalledWith(
308-
2,
309-
expect.objectContaining({
310-
amount: rentalPA.getCashFlowByDate(null),
311-
type: LedgerItemType.CashFlow,
312-
created: expectedToday,
313-
note: `for: ${rentalPA.address}, id: ${rentalPA.id} (${PropertyType[rentalPA.propertyType]})`,
314-
}),
315-
);
345+
Object.defineProperty(rentalPA, 'isOwned', {
346+
get: jest.fn().mockReturnValueOnce(true),
347+
});
348+
349+
//rentalPA.isOwned = true;
350+
rentalPA.getCashFlowByDate.mockReturnValueOnce(10);
351+
352+
user.monthlySavedAmount = 0;
353+
});
354+
355+
test('should not buy or sell properties', async () => {
356+
user.metMonthlyGoal.mockReturnValueOnce(true);
357+
user.hasMoneyToInvest.mockReturnValueOnce(true);
358+
rentalSFToPurchase.isAvailableByDate.mockReturnValueOnce(true);
359+
360+
const looper = (await import('../../src/time/looper')).looper;
361+
actual = looper(
362+
{
363+
propertyGeneratorSingleFamily: rentalGeneratorHome,
364+
propertyGeneratorPassiveApartment: rentalGeneratorPassive,
365+
},
366+
{
367+
user,
368+
endDate: new Date(),
369+
getBalance: jest.fn(),
370+
startDate: new Date(),
371+
rentals: [],
372+
clone: jest.fn().mockReturnThis(),
373+
getCashFlowMonthByEndDate: jest.fn(),
374+
},
375+
);
376+
377+
expect(actual.user.ledgerCollection.add).toHaveBeenCalledTimes(2);
378+
379+
expect(actual.user.hasMoneyToInvest).not.toHaveBeenCalled();
380+
});
316381
});
317382
});
318383

319384
describe('and sell properties', () => {
320385
beforeEach(async () => {
321386
user.hasMoneyToInvest.mockReturnValue(true);
322387
Object.defineProperty(rentalSF, 'isOwned', {
323-
get: jest.fn().mockReturnValue(true),
388+
get: jest.fn().mockReturnValueOnce(true),
324389
});
325390
rentalSF.getCashFlowByDate.mockReturnValue(0);
326391
rentalSF.canSell.mockReturnValue(true);
327392
rentalSF.getEquityFromSell.mockReturnValue(20);
328393

329394
Object.defineProperty(rentalPA, 'isOwned', {
330-
get: jest.fn().mockReturnValue(true),
395+
get: jest.fn().mockReturnValueOnce(true),
331396
});
332397
rentalPA.getCashFlowByDate.mockReturnValue(0);
333398
rentalPA.canSell.mockReturnValue(true);
@@ -380,20 +445,20 @@ describe('looper unit tests', () => {
380445

381446
describe('and buy properties', () => {
382447
beforeEach(() => {
383-
user.hasMoneyToInvest.mockReturnValue(true);
448+
user.hasMoneyToInvest.mockReturnValueOnce(true);
384449
Object.defineProperty(rentalSF, 'isOwned', {
385-
get: jest.fn().mockReturnValue(true),
450+
get: jest.fn().mockReturnValueOnce(true),
386451
});
387-
rentalSF.getCashFlowByDate.mockReturnValue(0);
388-
rentalSF.isAvailableByDate.mockReturnValue(true);
389-
rentalSF.getEquityFromSell.mockReturnValue(20);
452+
rentalSF.getCashFlowByDate.mockReturnValueOnce(0);
453+
rentalSF.isAvailableByDate.mockReturnValueOnce(true);
454+
rentalSF.getEquityFromSell.mockReturnValueOnce(20);
390455

391456
Object.defineProperty(rentalPA, 'isOwned', {
392-
get: jest.fn().mockReturnValue(true),
457+
get: jest.fn().mockReturnValueOnce(true),
393458
});
394-
rentalPA.getCashFlowByDate.mockReturnValue(0);
395-
rentalPA.isAvailableByDate.mockReturnValue(true);
396-
rentalPA.getEquityFromSell.mockReturnValue(2);
459+
rentalPA.getCashFlowByDate.mockReturnValueOnce(0);
460+
rentalPA.isAvailableByDate.mockReturnValueOnce(true);
461+
rentalPA.getEquityFromSell.mockReturnValueOnce(2);
397462

398463
getMinCostDownByRule.mockReturnValueOnce(8);
399464
getMinCostDownByRule.mockReturnValueOnce(80);
@@ -403,8 +468,8 @@ describe('looper unit tests', () => {
403468

404469
describe('and can do investment', () => {
405470
beforeEach(async () => {
406-
rentalSF.canInvestByUser.mockReturnValue({ canInvest: true, results: [] });
407-
rentalPA.canInvestByUser.mockReturnValue({ canInvest: true, results: [] });
471+
rentalSF.canInvestByUser.mockReturnValueOnce({ canInvest: true, results: [] });
472+
rentalPA.canInvestByUser.mockReturnValueOnce({ canInvest: true, results: [] });
408473

409474
const looper = (await import('../../src/time/looper')).looper;
410475
actual = looper(
@@ -450,11 +515,11 @@ describe('looper unit tests', () => {
450515
});
451516
describe('and can NOT do investment', () => {
452517
beforeEach(async () => {
453-
rentalSF.canInvestByUser.mockReturnValue({
518+
rentalSF.canInvestByUser.mockReturnValueOnce({
454519
canInvest: false,
455520
results: [{ message: chance.string(), investmentReason: InvestmentReasons.NoRules }],
456521
});
457-
rentalPA.canInvestByUser.mockReturnValue({
522+
rentalPA.canInvestByUser.mockReturnValueOnce({
458523
canInvest: false,
459524
results: [
460525
{
@@ -491,7 +556,9 @@ describe('looper unit tests', () => {
491556
});
492557

493558
test('should append reasons to passive', () => {
494-
const historicalPropertySF = actual.rentals.find((x) => x.property.propertyType === PropertyType.SingleFamily);
559+
const historicalPropertySF = actual.rentals.find(
560+
(x) => x.property.propertyType === PropertyType.SingleFamily && x.property.id === rentalSF.id,
561+
); //?
495562
const historicalPropertyPA = actual.rentals.find((x) => x.property.propertyType === PropertyType.PassiveApartment);
496563

497564
expect(historicalPropertySF.reasons.length).toEqual(1);

0 commit comments

Comments
 (0)