moneyfunx 3.0.9 → 3.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,8 +32,8 @@ export declare function determineCarryover(loan: Loan, loanPayment: number, loan
32
32
  *
33
33
  * @param {Loan} loan The loan to amortize payments for
34
34
  * @param {number} principal The amount borrowed
35
- * @param {number} payment The amount to pay to the loan's balance each period
36
- * @param {number} numPayments The number of periods to make payments to the loan
35
+ * @param {number|null} payment The amount to pay to the loan's balance each period
36
+ * @param {number|null} numPayments The number of periods to make payments to the loan
37
37
  * @param {number} startPeriod An initial offset of periods to 'fast-forward' the state of the loan to prior to calculation of each period
38
38
  * @param {number} carryover An additional amount to pay towards a loan, used when a residual amount is available from paying off the previous loan this period
39
39
  * @returns {PaymentRecord[]} The amortization schedule for the number of payments of payment made to the loan from the provided start period
@@ -1 +1 @@
1
- {"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../../../src/lib/debt/payments.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EACV,aAAa,EAEb,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EAAE,EACd,OAAO,EAAE,MAAM,GACd,MAAM,CAYR;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,EACxB,aAAa,EAAE,OAAO,GACrB,MAAM,CAOR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAAC,IAAI,EACpB,WAAW,EAAE,MAAM,GAAC,IAAI,EACxB,WAAW,GAAE,MAAU,EACvB,SAAS,GAAE,MAAU,GACpB,aAAa,EAAE,CA+BjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,EAAE,MAAM,EACf,aAAa,GAAE,OAAe,GAC7B,oBAAoB,CAqItB"}
1
+ {"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../../../src/lib/debt/payments.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EACV,aAAa,EAEb,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EAAE,EACd,OAAO,EAAE,MAAM,GACd,MAAM,CAYR;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,EACxB,aAAa,EAAE,OAAO,GACrB,MAAM,CAOR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,WAAW,GAAE,MAAU,EACvB,SAAS,GAAE,MAAU,GACpB,aAAa,EAAE,CA2BjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,EAAE,MAAM,EACf,aAAa,GAAE,OAAe,GAC7B,oBAAoB,CA6ItB"}
@@ -4,6 +4,7 @@
4
4
  *
5
5
  */
6
6
  import * as errors from '../errors.js';
7
+ import { TOTALS } from '../constants.js';
7
8
  /**
8
9
  *
9
10
  * Calculates the extra amount in a payment after all loans' minimum payments are met
@@ -45,27 +46,24 @@ export function determineCarryover(loan, loanPayment, loanFinalPayment, reduceMi
45
46
  *
46
47
  * @param {Loan} loan The loan to amortize payments for
47
48
  * @param {number} principal The amount borrowed
48
- * @param {number} payment The amount to pay to the loan's balance each period
49
- * @param {number} numPayments The number of periods to make payments to the loan
49
+ * @param {number|null} payment The amount to pay to the loan's balance each period
50
+ * @param {number|null} numPayments The number of periods to make payments to the loan
50
51
  * @param {number} startPeriod An initial offset of periods to 'fast-forward' the state of the loan to prior to calculation of each period
51
52
  * @param {number} carryover An additional amount to pay towards a loan, used when a residual amount is available from paying off the previous loan this period
52
53
  * @returns {PaymentRecord[]} The amortization schedule for the number of payments of payment made to the loan from the provided start period
53
54
  */
54
55
  export function amortizePayments(loan, principal, payment, numPayments, startPeriod = 0, carryover = 0) {
55
- if (payment === null) {
56
- payment = loan.minPayment;
57
- }
58
- payment = loan.validatePayment(payment);
59
- if (numPayments === null) {
60
- numPayments = loan.numPaymentsToZero(payment);
61
- }
56
+ // Strict null check handling
57
+ let actualPayment = (payment !== null) ? payment : loan.minPayment;
58
+ actualPayment = loan.validatePayment(actualPayment);
59
+ let actualNumPayments = (numPayments !== null) ? numPayments : loan.numPaymentsToZero(actualPayment);
62
60
  const amortizationSchedule = [];
63
61
  let principalRemaining = principal;
64
- for (let period = 0; period < numPayments; period++) {
62
+ for (let period = 0; period < actualNumPayments; period++) {
65
63
  const interestThisPeriod = loan.accrueInterest(principalRemaining);
66
- const principalThisPeriod = Math.min((period === numPayments - 1
67
- ? payment + carryover
68
- : payment) - interestThisPeriod, principalRemaining);
64
+ const principalThisPeriod = Math.min((period === actualNumPayments - 1
65
+ ? actualPayment + carryover
66
+ : actualPayment) - interestThisPeriod, principalRemaining);
69
67
  principalRemaining -= principalThisPeriod;
70
68
  amortizationSchedule.push({
71
69
  period: startPeriod + period + 1,
@@ -104,50 +102,76 @@ export function payLoans(loans, payment, reduceMinimum = false) {
104
102
  let totalLifetimePrincipal = 0;
105
103
  let totalAmortizationSchedule = [];
106
104
  while (paidLoans < loans.length) {
107
- const firstLoan = loans.slice(paidLoans)[0];
105
+ const activeLoans = loans.slice(paidLoans);
106
+ const firstLoan = activeLoans[0];
107
+ // 1. Determine the "Driver" parameters (the loan being paid off in this chunk)
108
108
  const firstLoanPayment = (firstLoan.minPayment +
109
- determineExtraPayment(loans.slice(paidLoans), monthlyPayment));
109
+ determineExtraPayment(activeLoans, monthlyPayment));
110
110
  const firstLoanPrincipalRemaining = loanPrincipalsRemaining[firstLoan.id];
111
+ // How long does this chunk last?
111
112
  const periodsToPay = firstLoan.numPaymentsToZero(firstLoanPayment, firstLoanPrincipalRemaining);
112
- const firstLoanAmortizedPayments = amortizePayments(firstLoan, firstLoanPrincipalRemaining, firstLoanPayment, periodsToPay, periodsElapsed);
113
- const firstLoanFinalPayment = firstLoan.principalRemaining(periodsToPay - 1, firstLoanPayment, firstLoanPrincipalRemaining) + firstLoan.accrueInterest(firstLoan.principalRemaining(periodsToPay - 1, firstLoanPayment, firstLoanPrincipalRemaining));
114
- paymentSchedule[firstLoan.id].amortizationSchedule = [
115
- ...paymentSchedule[firstLoan.id].amortizationSchedule,
116
- ...firstLoanAmortizedPayments
117
- ];
118
- totalAmortizationSchedule = [
119
- ...totalAmortizationSchedule,
120
- ...firstLoanAmortizedPayments
121
- ];
122
- paidLoans += 1;
123
- if (reduceMinimum) {
124
- monthlyPayment -= firstLoan.minPayment;
125
- }
126
- ;
127
- // handle calculating information for the rest of the loans
128
- loans.slice(paidLoans).forEach((loan, index) => {
113
+ // 2. Calculate the Final Payment specifics for the driver loan
114
+ // (Needed for carryover calculation)
115
+ const finalPrincipal = firstLoan.principalRemaining(periodsToPay - 1, firstLoanPayment, firstLoanPrincipalRemaining);
116
+ const firstLoanFinalPayment = finalPrincipal + firstLoan.accrueInterest(finalPrincipal);
117
+ // 3. Process ALL active loans for this chunk of time
118
+ activeLoans.forEach((loan, index) => {
119
+ const isDriverLoan = index === 0;
129
120
  const loanPrincipalRemaining = loanPrincipalsRemaining[loan.id];
130
- const loanAmortizedPayments = amortizePayments(loan, loanPrincipalRemaining, loan.minPayment, Math.min(periodsToPay, loan.numPaymentsToZero(loan.minPayment, loanPrincipalRemaining)), periodsElapsed, (index === 0 ? determineCarryover(firstLoan, firstLoanPayment, firstLoanFinalPayment, reduceMinimum) : 0));
121
+ // Determine payment amount: Driver gets extra, others get minimum
122
+ const paymentAmount = isDriverLoan ? firstLoanPayment : loan.minPayment;
123
+ // Determine duration: Others are capped by the Driver's time-to-zero
124
+ const duration = isDriverLoan
125
+ ? periodsToPay
126
+ : Math.min(periodsToPay, loan.numPaymentsToZero(loan.minPayment, loanPrincipalRemaining));
127
+ // Determine carryover: Only applies to the SECOND loan (index 1), derived from Driver
128
+ const carryoverAmount = (index === 1)
129
+ ? determineCarryover(firstLoan, firstLoanPayment, firstLoanFinalPayment, reduceMinimum)
130
+ : 0;
131
+ const loanAmortizedPayments = amortizePayments(loan, loanPrincipalRemaining, paymentAmount, duration, periodsElapsed, carryoverAmount);
132
+ // A. Append to individual loan schedule
131
133
  paymentSchedule[loan.id].amortizationSchedule = [
132
134
  ...paymentSchedule[loan.id].amortizationSchedule,
133
135
  ...loanAmortizedPayments,
134
136
  ];
135
- totalAmortizationSchedule = totalAmortizationSchedule.map((element) => {
136
- const matchedInnerElement = loanAmortizedPayments.find((innerElement) => innerElement.period === element.period);
137
- return (matchedInnerElement != null)
138
- ? {
139
- period: element.period,
140
- principal: element.principal + matchedInnerElement.principal,
141
- interest: element.interest + matchedInnerElement.interest,
142
- principalRemaining: element.principalRemaining +
143
- matchedInnerElement.principalRemaining
144
- }
145
- : element;
146
- });
147
- loanPrincipalsRemaining[loan.id] = paymentSchedule[loan.id].amortizationSchedule[paymentSchedule[loan.id].amortizationSchedule.length - 1].principalRemaining;
137
+ // B. Merge into Total Schedule
138
+ if (isDriverLoan) {
139
+ // The Driver extends the timeline. We append its records to initialize the new period slots in Totals.
140
+ totalAmortizationSchedule = [
141
+ ...totalAmortizationSchedule,
142
+ ...loanAmortizedPayments
143
+ ];
144
+ }
145
+ else {
146
+ // Followers merge into the existing slots created by the Driver.
147
+ // We only map over the NEW segment of the total schedule to avoid re-scanning history (optional optimization),
148
+ // but for safety/simplicity we map the whole structure and match by period.
149
+ totalAmortizationSchedule = totalAmortizationSchedule.map((element) => {
150
+ const matchedInnerElement = loanAmortizedPayments.find((innerElement) => innerElement.period === element.period);
151
+ return (matchedInnerElement != null)
152
+ ? {
153
+ period: element.period,
154
+ principal: element.principal + matchedInnerElement.principal,
155
+ interest: element.interest + matchedInnerElement.interest,
156
+ principalRemaining: element.principalRemaining + matchedInnerElement.principalRemaining
157
+ }
158
+ : element;
159
+ });
160
+ }
161
+ // Update tracking state for next iteration
162
+ if (paymentSchedule[loan.id].amortizationSchedule.length > 0) {
163
+ const fullSchedule = paymentSchedule[loan.id].amortizationSchedule;
164
+ loanPrincipalsRemaining[loan.id] = fullSchedule[fullSchedule.length - 1].principalRemaining;
165
+ }
148
166
  });
167
+ paidLoans += 1;
149
168
  periodsElapsed += periodsToPay;
169
+ if (reduceMinimum) {
170
+ monthlyPayment -= firstLoan.minPayment;
171
+ }
172
+ ;
150
173
  }
174
+ // Final Totals Calculation
151
175
  for (const loan of loans) {
152
176
  const loanLifetimeInterest = (paymentSchedule[loan.id].amortizationSchedule.reduce((lifetimeInterest, record) => lifetimeInterest + record.interest, 0));
153
177
  paymentSchedule[loan.id].lifetimeInterest = loanLifetimeInterest;
@@ -155,7 +179,7 @@ export function payLoans(loans, payment, reduceMinimum = false) {
155
179
  totalLifetimeInterest += loanLifetimeInterest;
156
180
  totalLifetimePrincipal += loan.currentBalance;
157
181
  }
158
- paymentSchedule.totals = {
182
+ paymentSchedule[TOTALS] = {
159
183
  lifetimeInterest: totalLifetimeInterest,
160
184
  lifetimePrincipal: totalLifetimePrincipal,
161
185
  amortizationSchedule: totalAmortizationSchedule,
@@ -4,7 +4,7 @@
4
4
  *
5
5
  */
6
6
  import type { Instrument } from '../investment/instrument.js';
7
- import { ContributionRecord, InstrumentsContributionSchedule } from '../investment/contributionTypes.js';
7
+ import type { ContributionRecord, InstrumentsContributionSchedule } from '../investment/contributionTypes.js';
8
8
  /**
9
9
  *
10
10
  * @param {IInstrument[]} instruments The instruments to allocate maximum contributions
@@ -19,8 +19,8 @@ export declare function determineExtraContribution(instruments: Instrument[], co
19
19
  * @param {number} contribution The amount to contribute to the instrument's balance
20
20
  * @param {number[int]} startPeriod The initial offset for period values
21
21
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
22
- * true: A -> C
23
- * false: C -> A
22
+ * true: A -> C
23
+ * false: C -> A
24
24
  * @returns {ContributionRecord} The amortized contribution
25
25
  */
26
26
  export declare function amortizeContribution(instrument: Instrument, currentBalance: number, contribution: number, startPeriod?: number, accrueBeforeContribution?: boolean): ContributionRecord;
@@ -34,8 +34,8 @@ export declare function amortizeContribution(instrument: Instrument, currentBala
34
34
  * @param {number} numContributions The number of periods to make contributions to the instrument
35
35
  * @param {number[int]} startPeriod The inital offset for period values
36
36
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
37
- * true: A -> C
38
- * false: C -> A
37
+ * true: A -> C
38
+ * false: C -> A
39
39
  * @returns {ContributionRecord[]} The amortized contributions
40
40
  */
41
41
  export declare function amortizeContributions(instrument: Instrument, initialBalance: number, contribution: number, numContributions: number, startPeriod?: number, accrueBeforeContribution?: boolean): ContributionRecord[];
@@ -45,8 +45,8 @@ export declare function amortizeContributions(instrument: Instrument, initialBal
45
45
  * @param {number} contribution The total amount to contirbute each period
46
46
  * @param {number[int]} numContributions The number of periods to contribute
47
47
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
48
- * true: A -> C
49
- * false: C -> A
48
+ * true: A -> C
49
+ * false: C -> A
50
50
  * @returns {InstrumentsContributionSchedule} The amortized contributions for all instruments
51
51
  */
52
52
  export declare function contributeInstruments(instruments: Instrument[], contribution: number, numContributions: number, accrueBeforeContribution?: boolean): InstrumentsContributionSchedule;
@@ -1 +1 @@
1
- {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/lib/investment/contributions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EACL,kBAAkB,EAClB,+BAA+B,EAGhC,MAAM,oCAAoC,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,UAAU,EAAE,EACzB,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,WAAW,GAAE,MAAU,EACvB,wBAAwB,GAAE,OAAc,GACvC,kBAAkB,CAgBpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,WAAW,GAAE,MAAU,EACvB,wBAAwB,GAAE,OAAc,GACvC,kBAAkB,EAAE,CAkBtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,UAAU,EAAE,EACzB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,wBAAwB,GAAE,OAAc,GACvC,+BAA+B,CA6FjC"}
1
+ {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/lib/investment/contributions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EACV,kBAAkB,EAClB,+BAA+B,EAGhC,MAAM,oCAAoC,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,UAAU,EAAE,EACzB,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,WAAW,GAAE,MAAU,EACvB,wBAAwB,GAAE,OAAc,GACvC,kBAAkB,CAkBpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,WAAW,GAAE,MAAU,EACvB,wBAAwB,GAAE,OAAc,GACvC,kBAAkB,EAAE,CAmBtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,UAAU,EAAE,EACzB,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,wBAAwB,GAAE,OAAc,GACvC,+BAA+B,CAyGjC"}
@@ -3,6 +3,7 @@
3
3
  * This file containts functions for computing detailed information on contributing to investments
4
4
  *
5
5
  */
6
+ import { TOTALS } from '../constants.js';
6
7
  /**
7
8
  *
8
9
  * @param {IInstrument[]} instruments The instruments to allocate maximum contributions
@@ -21,26 +22,27 @@ export function determineExtraContribution(instruments, contribution) {
21
22
  * @param {number} contribution The amount to contribute to the instrument's balance
22
23
  * @param {number[int]} startPeriod The initial offset for period values
23
24
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
24
- * true: A -> C
25
- * false: C -> A
25
+ * true: A -> C
26
+ * false: C -> A
26
27
  * @returns {ContributionRecord} The amortized contribution
27
28
  */
28
29
  export function amortizeContribution(instrument, currentBalance, contribution, startPeriod = 0, accrueBeforeContribution = true) {
29
30
  let interestThisPeriod;
31
+ let newBalance = currentBalance;
30
32
  if (accrueBeforeContribution) {
31
- interestThisPeriod = instrument.accrueInterest(currentBalance);
32
- currentBalance += contribution + interestThisPeriod;
33
+ interestThisPeriod = instrument.accrueInterest(newBalance);
34
+ newBalance += contribution + interestThisPeriod;
33
35
  }
34
36
  else {
35
- currentBalance += contribution;
36
- interestThisPeriod = instrument.accrueInterest(currentBalance);
37
- currentBalance += interestThisPeriod;
37
+ newBalance += contribution;
38
+ interestThisPeriod = instrument.accrueInterest(newBalance);
39
+ newBalance += interestThisPeriod;
38
40
  }
39
41
  return {
40
42
  period: startPeriod + 1,
41
43
  contribution: contribution,
42
44
  growth: interestThisPeriod,
43
- currentBalance,
45
+ currentBalance: newBalance,
44
46
  };
45
47
  }
46
48
  ;
@@ -54,8 +56,8 @@ export function amortizeContribution(instrument, currentBalance, contribution, s
54
56
  * @param {number} numContributions The number of periods to make contributions to the instrument
55
57
  * @param {number[int]} startPeriod The inital offset for period values
56
58
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
57
- * true: A -> C
58
- * false: C -> A
59
+ * true: A -> C
60
+ * false: C -> A
59
61
  * @returns {ContributionRecord[]} The amortized contributions
60
62
  */
61
63
  export function amortizeContributions(instrument, initialBalance, contribution, numContributions, startPeriod = 0, accrueBeforeContribution = true) {
@@ -66,6 +68,7 @@ export function amortizeContributions(instrument, initialBalance, contribution,
66
68
  const periodicContribution = instrument.validateContribution(contribution, ytd);
67
69
  const record = amortizeContribution(instrument, currentBalance, periodicContribution, period + startPeriod, accrueBeforeContribution);
68
70
  currentBalance = record.currentBalance;
71
+ // Reset YTD every 12 periods
69
72
  period % 12 === 0 ? ytd = 0 : ytd += periodicContribution;
70
73
  contributionSchedule.push(record);
71
74
  }
@@ -78,8 +81,8 @@ export function amortizeContributions(instrument, initialBalance, contribution,
78
81
  * @param {number} contribution The total amount to contirbute each period
79
82
  * @param {number[int]} numContributions The number of periods to contribute
80
83
  * @param {boolean} accrueBeforeContribution A flag for ordering operations of accrual (A) and contribution (C)
81
- * true: A -> C
82
- * false: C -> A
84
+ * true: A -> C
85
+ * false: C -> A
83
86
  * @returns {InstrumentsContributionSchedule} The amortized contributions for all instruments
84
87
  */
85
88
  export function contributeInstruments(instruments, contribution, numContributions, accrueBeforeContribution = true) {
@@ -126,22 +129,37 @@ export function contributeInstruments(instruments, contribution, numContribution
126
129
  contributionSchedules[instrument.id].lifetimeGrowth = instrumentLifetimeGrowth;
127
130
  totalLifetimeContribution += instrumentLifetimeContribution;
128
131
  totalLifetimeGrowth += instrumentLifetimeGrowth;
129
- totalAmortizationSchedule = totalAmortizationSchedule.length
130
- ? (totalAmortizationSchedule.map((element) => {
131
- const matchedInnerElement = instrumentSchedule.find((innerElement) => innerElement.period === element.period);
132
- return (matchedInnerElement != null)
133
- ? {
134
- period: element.period,
135
- contribution: element.contribution + matchedInnerElement.contribution,
136
- growth: element.growth + matchedInnerElement.growth,
137
- currentBalance: element.currentBalance +
138
- matchedInnerElement.currentBalance
139
- }
140
- : element;
141
- }))
142
- : instrumentSchedule;
132
+ // Merge totals safely handling varying lengths
133
+ if (totalAmortizationSchedule.length === 0) {
134
+ // Initialize with the first instrument's schedule
135
+ totalAmortizationSchedule = [...instrumentSchedule];
136
+ }
137
+ else {
138
+ // We need to merge. If schedules are different lengths, we map over the longer one.
139
+ const maxLen = Math.max(totalAmortizationSchedule.length, instrumentSchedule.length);
140
+ const newTotal = [];
141
+ for (let i = 0; i < maxLen; i++) {
142
+ const totalRecord = totalAmortizationSchedule[i];
143
+ const instrumentRecord = instrumentSchedule[i];
144
+ if (totalRecord && instrumentRecord) {
145
+ newTotal.push({
146
+ period: totalRecord.period,
147
+ contribution: totalRecord.contribution + instrumentRecord.contribution,
148
+ growth: totalRecord.growth + instrumentRecord.growth,
149
+ currentBalance: totalRecord.currentBalance + instrumentRecord.currentBalance
150
+ });
151
+ }
152
+ else if (totalRecord) {
153
+ newTotal.push(totalRecord);
154
+ }
155
+ else if (instrumentRecord) {
156
+ newTotal.push(instrumentRecord);
157
+ }
158
+ }
159
+ totalAmortizationSchedule = newTotal;
160
+ }
143
161
  }
144
- contributionSchedules.totals = {
162
+ contributionSchedules[TOTALS] = {
145
163
  lifetimeContribution: totalLifetimeContribution,
146
164
  lifetimeGrowth: totalLifetimeGrowth,
147
165
  amortizationSchedule: totalAmortizationSchedule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moneyfunx",
3
- "version": "3.0.9",
3
+ "version": "3.0.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {