The Code
const processForm = (event) => {
event.preventDefault();
const detailsSection = document.getElementById('detailsSection');
const inputForm = document.querySelector(".input-form");
const parsedData = new FormData(inputForm);
let formEntries = Object.fromEntries(parsedData.entries());
formEntries = convertToNumbers(formEntries);
const formOk = validateEntries(formEntries);
let summary = calculateSummary(formEntries);
if (summary) {
let payments = calculatePayments(summary);
displayPayments(payments);
displaySummary(summary);
}
setTimeout(() => {detailsSection.scrollIntoView()});
}
const calculatePayments = summary => {
const {monthlyPayment, interestRate, totalCost, termLength, principal} = summary;
let remainingPrincipal = principal;
let totalInterest = 0;
const tableData = [];
let lastPayment = false;
for (let i = 0; i < termLength; i++) {
const breakdown = {
month: 0,
payment: 0,
principal: 0,
interest: 0,
totalInterest: 0,
balance: 0
}
breakdown.month = i + 1;
breakdown.interest = (interestRate/1200) * remainingPrincipal;
totalInterest += breakdown.interest;
breakdown.totalInterest = totalInterest;
if (lastPayment) {
breakdown.balance = remainingPrincipal;
breakdown.principal = breakdown.balance;
breakdown.payment = breakdown.principal + breakdown.interest;
remainingPrincipal -= breakdown.principal;
breakdown.balance = remainingPrincipal;
tableData.push(breakdown);
return tableData
}
breakdown.payment = monthlyPayment;
breakdown.principal = breakdown.payment - breakdown.interest;
remainingPrincipal -= breakdown.principal;
breakdown.balance = remainingPrincipal;
if (breakdown.balance < breakdown.principal) {
lastPayment = true;
}
if (breakdown.balance <= 0) {
breakdown.balance = 0;
}
tableData.push(breakdown);
}
return tableData;
}
const displayPayments = payments => {
const table = document.getElementById('paymentsTable');
const tableHeading = table.querySelector('thead');
const headerTemplate = document.getElementById('tableHeadingTemplate');
const headerContent = headerTemplate.content.cloneNode(true);
tableHeading.innerHTML = '';
tableHeading.appendChild(headerContent);
const tableBody = table.querySelector('tbody');
const rowTemplate = document.getElementById('tableRowTemplate');
tableBody.innerHTML = '';
for (let i = 0; i < payments.length; i++) {
const paymentObj = payments[i];
const {month, payment, principal, interest, totalInterest, balance} = paymentObj;
const rowContent = rowTemplate.content.cloneNode(true);
const formatOptions = {
style: "currency",
currency: "USD"
};
const tableCells = rowContent.querySelectorAll('td');
tableCells[0].textContent = month;
tableCells[1].textContent = payment.toLocaleString('en-US', formatOptions);
tableCells[2].textContent = principal.toLocaleString('en-US', formatOptions);
tableCells[3].textContent = interest.toLocaleString('en-US', formatOptions);
tableCells[4].textContent = totalInterest.toLocaleString('en-US', formatOptions);
tableCells[5].textContent = balance.toLocaleString('en-US', formatOptions);
tableBody.appendChild(rowContent);
}
}
const calculateSummary = entries => {
const {loanAmount, termLength, interestRate, targetPayment} = entries;
const summary = {
monthlyPayment: 0,
principal: 0,
interest: 0,
totalCost: 0,
downPayment: 0,
termLength,
interestRate
};
const minimumPayment = ((loanAmount * (interestRate/1200)) / (1 - (1 + interestRate/1200) ** (-1*termLength)));
if (!targetPayment) {
summary.monthlyPayment = Number(minimumPayment);
summary.principal = loanAmount;
summary.totalCost = (summary.monthlyPayment * termLength);
summary.interest = (summary.totalCost - summary.principal);
} else if (minimumPayment > Number(targetPayment)) {
summary.monthlyPayment = Number(targetPayment);
const minimumCost = minimumPayment * termLength;
summary.totalCost = targetPayment * termLength;
summary.downPayment = minimumCost - summary.totalCost;
summary.principal = loanAmount - summary.downPayment;
summary.interest = summary.totalCost - summary.principal;
} else {
const alertMessage = `You won't need to pay any downpayment at any amount above ${minimumPayment.toFixed(2)}$. To still opt for ${targetPayment.toFixed(2)}$, consider reducing the term length.`
showAlert(alertMessage,"Good News", "success");
clearTable();
clearSummary();
insertLogo();
return;
}
return summary;
}
const displaySummary = summary => {
const {monthlyPayment, principal, interest, totalCost, downPayment} = summary;
clearSummary()
const detailsTemplate = document.getElementById('detailsTemplate');
const detailsElement = detailsTemplate.content.cloneNode(true);
const paymentAmount = detailsElement.querySelector('.payment-headline-amount');
const principalAmount = detailsElement.getElementById('principalAmount');
const interestAmount = detailsElement.getElementById('interestAmount');
const costAmount = detailsElement.getElementById('costAmount');
const finalAmount = detailsElement.getElementById('finalAmount');
const formatOptions = {
style: "currency",
currency: "USD"
};
paymentAmount.innerText = monthlyPayment.toLocaleString('en-US', formatOptions);
principalAmount.innerText = principal.toLocaleString('en-US', formatOptions);
interestAmount.innerText = interest.toLocaleString('en-US', formatOptions);
costAmount.innerText = totalCost.toLocaleString('en-US', formatOptions);
finalAmount.innerText = downPayment.toLocaleString('en-US', formatOptions);
detailsSection.appendChild(detailsElement);
}
const clearSummary = () => {
const detailsSection = document.getElementById('detailsSection');
detailsSection.innerHTML = '';
}
const insertLogo = () => {
const detailsSection = document.getElementById('detailsSection');
detailsSection.innerHTML = '<img class="img-fluid opacity-50" src="/img/paycal wallet.svg" style="max-height: 10rem" />';
}
const clearTable = () => {
const table = document.getElementById('paymentsTable');
const tableHeading = table.querySelector('thead');
const tableBody = table.querySelector('tbody');
tableHeading.innerHTML = '';
tableBody.innerHTML = '';
}
const convertToNumbers = entries => {
const refinedEntries = {
loanAmount: Number(entries.loanAmount),
termLength: Number(entries.termLength),
interestRate: Number(entries.interestRate),
}
if (entries.targetPayment) {
refinedEntries.targetPayment = Number(entries.targetPayment);
} else {
refinedEntries.targetPayment = "";
}
return refinedEntries;
}
const validateEntries = entries => {
const {loanAmount, termLength, interestRate, targetPayment} = entries;
const loanOk = !isNaN(loanAmount) && loanAmount > 0;
const termOk = Number.isInteger(termLength) && termLength > 0;
const interestOk = !isNaN(interestRate) && interestRate > 0;
const targetOk = Number.isInteger(targetPayment) || targetPayment == "";
const shortTerm = termLength < 12;
const rateLow = interestRate < 1;
const rateHigh = interestRate > 14;
const lowAmount = loanAmount < 2000;
if (!loanOk) {
showAlert('Please enter a valid loan amount.', "Oops", "error");
} else if (!termOk) {
showAlert('Please enter a valid term.', "Oops", "error");
} else if (!interestOk) {
showAlert('Please enter a valid interest rate.', "Oops", "error");
} else if (!targetOk) {
showAlert('Please enter a valid target rate.', "Oops", "error");
} else if (shortTerm) {
showAlert("Did you enter the loan term in months?", "Just to Confirm", "warning")
return true;
} else if (rateLow) {
showAlert("Please confirm that you have not converted the interest rate to a decimal.", "Just to Confirm", "warning")
return true;
} else if (rateHigh) {
showAlert("We noticed a double digit interest rate. Please confirm if it's correct.", "Just to Confirm", "warning")
return true;
} else if (lowAmount) {
showAlert("Kindly confirm that you've entered the right figure for the loan amount?", "Just to Confirm", "warning");
return true;
} else {
return true;
}
}
const showAlert = (message, heading, type) => {
Swal.fire({
backdrop: false,
title: heading,
text: message,
icon: type,
confirmButtonColor: '#253439'
})
}
Abstract
For this app: I had my tasks divided up into the following pointers:
- Get the values that the user entered and convert them to the type Number.
- Validate the data. Display a helpful error or a warning depending on the user input.
- If the data is valid, calculate the values for the summary section.
- Calculate the payments for the table.
- Display the summary and the payments in the relevant sections.
Calculation Functions
The two functions actually doing the calculations are as follows:
calculateSummary(entries) (line 102): This function takes in a parameter of
entries
. I destructured the object entries
so that I can easily access the
properties. I then initialize an object summary
which has relevant properties I'd want later in
the code. I calculate the minimumPayment
which is the monthly payment for a given term and
interest rate if there's no downpayment.
Then I had 3 different scenarios to consider.
- (line 116) If user did not specify a target value.
-
(line 121) If user specified a target payment which is more than the
minimumPayment
. In other words, a downpayment would be required. - (line 128) Finally, if a user specified a target payment which is more than the minimum payment required anyway, a message is displayed informing the user of the same and probing them to alter the term instead. This condition does not continue any calculations/display.
calculatePayments(summary) (line 18): This function takes in an object
summary
to generate all the payments, store them in an array which is returned by the function.
For this function, mostly all values of interest were present in the summary
object but other
than that I also relied on running calculations of totalInterest
(line 21) and
remainingPrincipal
(line 20).
This function was pretty much straight forward except for an edge case scenario. If the user opted for a
target payment, the final payment could end up being less than the otherwise stated monthly payment. I used
the functionally scoped boolean lastPayment
. If this was true (line 40), I treat the
breakdown
object's properties differently. the interest would be reliable as it'd be calculated
on the remaining balance only. But the principal
would be the same as the
balance
instead of the regular monthlyPayment - interest
. By adding the
principal
and interest
, I calculate the payment
for this month. I
also reduce the remainingPrincipal
variable and assign it to the balance
property.
Finally I add this breakdown to the tableData
array and return it within the codeblock as this
is the last payment and may not be the last month of the term so we want to avoid the full loop.
Display Functions
There's two display functions that I used. displaySummary
takes in the return value of
calculateSummary
whereas displayPayments
takes in the return value of
calculatePayments
. Both functions rely on the template tag. They clone a template tag, fill it
up using the data they take in as parameters, and finally append the elements into the DOM.
Data Validation
I coupled html attributes and JS error handling to ensure I was working with desired data.
- In the HTML form, I specified the data types for each input so it ensured that the user did submit acceptable data.
-
Next, I used
convertToNumbers
function to convert the string values to required data. An optional field likeTarget Payment
could be both an empty string and a number. I ensured that the function allows for both. -
Then, I passed the data through
validateEntries
and ensured that the numbers were positive and of the rightdata type
. -
Additionally, just to help the user, I display alert messages at abnormal inputs like
term < 12
,
rate < 1
,rate > 14
andloanAmount > 2000
. The payments are still calculated. These just ensure a positive user experience.