moneyfunx 3.0.10 → 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.
@@ -1 +1 @@
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,CA0ItB"}
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"}
@@ -102,58 +102,76 @@ export function payLoans(loans, payment, reduceMinimum = false) {
102
102
  let totalLifetimePrincipal = 0;
103
103
  let totalAmortizationSchedule = [];
104
104
  while (paidLoans < loans.length) {
105
- 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)
106
108
  const firstLoanPayment = (firstLoan.minPayment +
107
- determineExtraPayment(loans.slice(paidLoans), monthlyPayment));
109
+ determineExtraPayment(activeLoans, monthlyPayment));
108
110
  const firstLoanPrincipalRemaining = loanPrincipalsRemaining[firstLoan.id];
111
+ // How long does this chunk last?
109
112
  const periodsToPay = firstLoan.numPaymentsToZero(firstLoanPayment, firstLoanPrincipalRemaining);
110
- const firstLoanAmortizedPayments = amortizePayments(firstLoan, firstLoanPrincipalRemaining, firstLoanPayment, periodsToPay, periodsElapsed);
111
- // Calculate final payment logic
113
+ // 2. Calculate the Final Payment specifics for the driver loan
114
+ // (Needed for carryover calculation)
112
115
  const finalPrincipal = firstLoan.principalRemaining(periodsToPay - 1, firstLoanPayment, firstLoanPrincipalRemaining);
113
116
  const firstLoanFinalPayment = finalPrincipal + firstLoan.accrueInterest(finalPrincipal);
114
- paymentSchedule[firstLoan.id].amortizationSchedule = [
115
- ...paymentSchedule[firstLoan.id].amortizationSchedule,
116
- ...firstLoanAmortizedPayments
117
- ];
118
- // Merge into totals
119
- totalAmortizationSchedule = [
120
- ...totalAmortizationSchedule,
121
- ...firstLoanAmortizedPayments
122
- ];
123
- paidLoans += 1;
124
- if (reduceMinimum) {
125
- monthlyPayment -= firstLoan.minPayment;
126
- }
127
- ;
128
- // handle calculating information for the rest of the loans
129
- loans.slice(paidLoans).forEach((loan, index) => {
117
+ // 3. Process ALL active loans for this chunk of time
118
+ activeLoans.forEach((loan, index) => {
119
+ const isDriverLoan = index === 0;
130
120
  const loanPrincipalRemaining = loanPrincipalsRemaining[loan.id];
131
- 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
132
133
  paymentSchedule[loan.id].amortizationSchedule = [
133
134
  ...paymentSchedule[loan.id].amortizationSchedule,
134
135
  ...loanAmortizedPayments,
135
136
  ];
136
- // Safe map for totals
137
- totalAmortizationSchedule = totalAmortizationSchedule.map((element) => {
138
- const matchedInnerElement = loanAmortizedPayments.find((innerElement) => innerElement.period === element.period);
139
- return (matchedInnerElement != null)
140
- ? {
141
- period: element.period,
142
- principal: element.principal + matchedInnerElement.principal,
143
- interest: element.interest + matchedInnerElement.interest,
144
- principalRemaining: element.principalRemaining +
145
- matchedInnerElement.principalRemaining
146
- }
147
- : element;
148
- });
149
- // Update remaining principals for next iteration
150
- const currentLoanSchedule = paymentSchedule[loan.id].amortizationSchedule;
151
- if (currentLoanSchedule.length > 0) {
152
- loanPrincipalsRemaining[loan.id] = currentLoanSchedule[currentLoanSchedule.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;
153
165
  }
154
166
  });
167
+ paidLoans += 1;
155
168
  periodsElapsed += periodsToPay;
169
+ if (reduceMinimum) {
170
+ monthlyPayment -= firstLoan.minPayment;
171
+ }
172
+ ;
156
173
  }
174
+ // Final Totals Calculation
157
175
  for (const loan of loans) {
158
176
  const loanLifetimeInterest = (paymentSchedule[loan.id].amortizationSchedule.reduce((lifetimeInterest, record) => lifetimeInterest + record.interest, 0));
159
177
  paymentSchedule[loan.id].lifetimeInterest = loanLifetimeInterest;
@@ -1 +1 @@
1
- {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/lib/investment/contributions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,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,CAoBtB;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,CA+FjC"}
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"}
@@ -68,8 +68,7 @@ export function amortizeContributions(instrument, initialBalance, contribution,
68
68
  const periodicContribution = instrument.validateContribution(contribution, ytd);
69
69
  const record = amortizeContribution(instrument, currentBalance, periodicContribution, period + startPeriod, accrueBeforeContribution);
70
70
  currentBalance = record.currentBalance;
71
- // Reset YTD every 12 periods, assuming monthly periodicity matching periodsPerYear
72
- // NOTE: This logic assumes period 0 is the start of a year.
71
+ // Reset YTD every 12 periods
73
72
  period % 12 === 0 ? ytd = 0 : ytd += periodicContribution;
74
73
  contributionSchedule.push(record);
75
74
  }
@@ -130,21 +129,35 @@ export function contributeInstruments(instruments, contribution, numContribution
130
129
  contributionSchedules[instrument.id].lifetimeGrowth = instrumentLifetimeGrowth;
131
130
  totalLifetimeContribution += instrumentLifetimeContribution;
132
131
  totalLifetimeGrowth += instrumentLifetimeGrowth;
133
- // Merge totals safely
134
- totalAmortizationSchedule = totalAmortizationSchedule.length
135
- ? (totalAmortizationSchedule.map((element) => {
136
- const matchedInnerElement = instrumentSchedule.find((innerElement) => innerElement.period === element.period);
137
- return (matchedInnerElement != null)
138
- ? {
139
- period: element.period,
140
- contribution: element.contribution + matchedInnerElement.contribution,
141
- growth: element.growth + matchedInnerElement.growth,
142
- currentBalance: element.currentBalance +
143
- matchedInnerElement.currentBalance
144
- }
145
- : element;
146
- }))
147
- : 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
+ }
148
161
  }
149
162
  contributionSchedules[TOTALS] = {
150
163
  lifetimeContribution: totalLifetimeContribution,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moneyfunx",
3
- "version": "3.0.10",
3
+ "version": "3.0.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {