UNPKG

loan

Version:

Tool for making calculations on loans

528 lines (420 loc) 18.3 kB
var _ = require('lodash'); var moment = require('moment'); var should = require('should'); var Loan = require('./../lib/loan'); var fixtures = require('./fixtures/loan.json'); describe('Loan', function() { it('should export utils', function() { Loan.Utils.should.exist; Loan.utils.should.exist; }); it('should export format', function() { Loan.Format.should.exist; Loan.format.should.exist; }); describe('instance', function() { var loan = null; beforeEach(function() { loan = new Loan(); }); it('should return an instance of Loan when not called with new', function() { Loan().should.be.instanceof(Loan); }); it('should have some defaults from getDefaults', function() { loan.should.containDeep(_.omit(loan.getDefaults(), 'as_of')); }); describe('getPaymentPlan', function() { beforeEach(function() { loan = new Loan(fixtures.short); }); it('should return an empty array when instalments is 0', function() { loan.instalments = 0; loan.getPaymentPlan().should.be.instanceof(Array).and.have.lengthOf(0); }); it('should return an array with a length equal to instalments here the first is equal to a calculated copy of the loan', function() { loan.instalments = 4; var plan = loan.getPaymentPlan(); plan.should.be.instanceof(Array).and.have.lengthOf(4); Loan.Format.round((plan[0]), 0).should.containDeep(_.pick(loan.round(0), 'instalments', 'principal', 'interest_rate', 'initial_fee', 'invoice_fee')); loan.type = 'serial'; loan.getPaymentPlan().should.be.instanceof(Array).and.have.lengthOf(4); }); it('should return the correct payment plan for a long annuity loan', function() { loan = new Loan(fixtures.long); var plan = loan.getPaymentPlan(); plan.should.have.lengthOf(120); var last = Loan.Format.round(_.last(plan), 0); last.should.containDeep({ interest_rate: loan.interest_rate, interest: 11, to_pay: 2382, principal: 2382 - 11, as_of: loan.getDateOfInstalment(last.instalment) }); }); it('should return the correct payment plan for a long serial loan', function() { loan = new Loan(_.extend({}, fixtures.long, {type: 'serial'})); var plan = loan.getPaymentPlan(); plan.should.have.lengthOf(120); var last = Loan.Format.round(_.last(plan), 0); last.should.containDeep({ interest_rate: loan.interest_rate, interest: 8, to_pay: 1842, principal: 1841 - 8, as_of: loan.getDateOfInstalment(last.instalment) }); }); it('should return a commutative series', function() { loan = new Loan(fixtures.long); loan.instalments = 10; var plan = loan.getPaymentPlan(); var index = Math.floor(plan.length / 2); var nloan = new Loan(plan[index]); var nplan = nloan.getPaymentPlan(); var omit = ['as_of', 'instalments']; // will vary and can't be compared plan = plan.slice(index).map(function(p) { return Loan.Format.round(_.omit(p, omit), 0) }); nplan = nplan.map(function(p) { return Loan.Format.round(_.omit(p, omit), 0); }); plan.should.containDeepOrdered(nplan); }); it('should return the correct payment plan for an annuity loan without interest', function() { loan = new Loan(fixtures.short); loan.interest_rate = 0; var plan = loan.getPaymentPlan(); plan.should.have.lengthOf(2); plan[1].should.have.property('principal', fixtures.short.principal / 2); }); }); describe('getDateOfInstalment', function() { it('should return null if instalment is not given', function() { should(loan.getDateOfInstalment()).equal(null); }); it('should return null if as_of and pay_every is not set', function() { delete loan.as_of, loan.pay_every; should(loan.getDateOfInstalment()).equal(null); }); it('should return the start of pay_every for the 0th (current) instalment', function() { var m = moment().subtract(1, 'second'); loan.as_of = m.toDate(); loan.pay_every = 'month'; loan.getDateOfInstalment(0).should.eql(m.clone().startOf('month').toDate()); loan.pay_every = 'quarter'; loan.getDateOfInstalment(0).should.eql(m.clone().startOf('quarter').toDate()); loan.pay_every = 'year'; loan.getDateOfInstalment(0).should.eql(m.clone().startOf('year').toDate()); }); it('should return successive instalments at the start of pay_every', function() { var m = moment().subtract(1, 'second'); loan.as_of = m.toDate(); loan.pay_every = 'month'; loan.getDateOfInstalment(9).should.eql(m.clone().add(9, 'months').startOf('month').toDate()); }); }); describe('getTotalCost', function() { beforeEach(function() { loan = new Loan(fixtures.long); }); it('should return 0 when instalments is 0', function() { loan.instalments = 0; loan.getTotalCost().should.equal(0); }); it('should not return NaN when having missing properties', function() { var loan = new Loan({principal: 10000, instalments: 120}); loan.getTotalCost().should.be.above(-1); }); it('should return the total cost long annuity loan', function() { loan.getTotalCost().should.be.approximately(66351, 0.5); }); it('should return the total cost long serial loan', function() { loan.type = 'serial'; loan.getTotalCost().should.be.approximately(60945, 0.5); }); }); describe('getInterestRatePerInstalment', function() { beforeEach(function() { loan = new Loan(fixtures.short); }); it('should return 0 when interest_rate is invalid', function() { loan.interest_rate = null; loan.getInterestRatePerInstalment().should.equal(0); }); it('should return 0 when pay_every is invalid', function() { loan.pay_every = 'asdf'; loan.getInterestRatePerInstalment().should.equal(0); }); it('should return the correct rate when pay_every is month', function() { loan.pay_every = 'month'; loan.getInterestRatePerInstalment().should.equal(loan.interest_rate / 12); }); it('should return the correct rate when pay_every is quarter', function() { loan.pay_every = 'quarter'; loan.getInterestRatePerInstalment().should.equal(loan.interest_rate / 4); }); it('should return the correct rate when pay_every is year', function() { loan.pay_every = 'year'; loan.getInterestRatePerInstalment().should.equal(loan.interest_rate); }); }); describe('getEffectiveInterestRate', function() { beforeEach(function() { loan = new Loan(fixtures.long); }); it('should return 0 when instalments is 0', function() { loan.instalments = 0; loan.getEffectiveInterestRate().should.equal(0); }); it('should return the correct effective rate for a long annuity loan', function() { var rate = 0.0564; var erate = loan.getEffectiveInterestRate(); erate.should.be.approximately(rate, 0.0005); }); it('should return the correct effective rate for another annuity loan', function() { loan = new Loan({ principal: 55000, instalments: 12 * 12, initial_fee: 495, invoice_fee: 35, interest_rate: 0.159 }); var erate = loan.getEffectiveInterestRate(); erate.should.be.approximately(0.1848, 0.0005); }); it('should return the correct effective rate for the loan in issue #5', function() { loan = new Loan({ type: 'annuity', pay_every: 'month', principal: 200000, interest_rate: 0.07, invoice_fee: 100, instalments: 200 }); loan.getEffectiveInterestRate().should.be.approximately(0.0817, 0.0005); }); }); describe('shouldAmortize', function() { beforeEach(function() { loan = new Loan(fixtures.long); loan.as_of = new Date(); }); it('should return false when as_of is not set', function() { delete loan.as_of; loan.shouldAmortize().should.equal(false); }); it('should return false when as_of is within the current month or in the future', function() { loan.shouldAmortize().should.equal(false); loan.as_of = moment().add(1, 'year').toDate(); loan.shouldAmortize().should.equal(false); }); it('should return false when instalments is 0', function() { loan.as_of = moment().subtract(3, 'months').toDate(); loan.instalments = 0; loan.shouldAmortize().should.equal(false); }); it('should return false when principal is 0', function() { loan.as_of = moment().subtract(3, 'months').toDate(); loan.principal = 0; loan.shouldAmortize().should.equal(false); }); it('should return true when as_of is a month previous and before', function() { loan.as_of = moment().subtract(3, 'months').toDate(); loan.shouldAmortize().should.equal(true); }); it('should be passed a date as a parameter to compare to, instead of now', function() { var date = moment().subtract(3, 'months').toDate(); loan.as_of = date; loan.shouldAmortize(date).should.equal(false); }); }); describe('calculateMeta', function() { it('should calculate a number of meta fields', function() { loan = new Loan(fixtures.short); loan.data.interest_rate_effective = 666; // test unlocked loan loan.calculateMeta(); var firstPlan = _.first(loan.getPaymentPlan()); loan.data.should.containDeep({ interest_rate_effective: loan.getEffectiveInterestRate(), should_amortize: loan.shouldAmortize(), monthly_cost: firstPlan.to_pay + firstPlan.invoice_fee, total_cost: loan.getTotalCost() }); }); it('should not calculate any non-present meta fields if the loan is locked', function() { var data = {interest_rate_effective: 999, total_cost: 456, should_amortize: false}; loan = new Loan(_.extend({}, fixtures.short, {data: data})); loan.locked = true; loan.data.should.not.have.property('monthly_cost'); var ndata = loan.calculateMeta(); loan.data.should.containDeep(data); loan.data.should.have.property('monthly_cost', ndata.monthly_cost); }); }); describe('getAsOf', function() { it('should return as_of if it is set', function() { var now = moment().startOf('month').toDate(); var loan = new Loan({as_of: now}); loan.getAsOf().should.equal(now); }); it('should return the current date to second precision if not set', function(done) { var now = new Date(); var loan = new Loan(); setTimeout(function() { loan.getAsOf().should.eql(moment(now).startOf('second').toDate()); done(); }, 50); }); }); describe('amortize', function() { var stableFields = ['interest_rate', 'principal', 'pay_every', 'intial_fee', 'invoice_fee', 'instalments']; beforeEach(function() { loan = new Loan(fixtures.long); loan.as_of = new Date(); }); context('by date', function() { it('should return a copy of the same loan when when as_of is the current month or not set', function() { loan.amortize().should.containDeep(_.pick(loan.toJSON(), stableFields)); delete loan.as_of; loan.amortize().should.containDeep(_.pick(loan.toJSON(), stableFields)); }); it('should return a loan that corresponds to the payment plan of the amount of months up to today', function() { loan.as_of = moment().subtract(3, 'months').toDate(); var plan = loan.getPaymentPlan(); loan.amortize().should.containDeep(_.omit(plan[3], 'data')); }); it('should be passed a date as a parameter and return the loan up to those months', function() { var plan = loan.getPaymentPlan(); var date = moment().add(3, 'months').toDate(); loan.amortize(date).should.containDeep(_.omit(plan[3], 'data')); }); it('should return a loan with 0 principal and interest instalments are 0', function() { loan.instalments = 0; loan.amortize().should.containDeep({principal: 0, interest_rate: 0, instalments: 0}); }); it('should return a loan with 0 principal when amortization is further away than bounds', function() { loan.as_of = moment().subtract(5, 'months').toDate(); loan.instalments = 3; loan.amortize().should.containDeep({ principal: 0, interest_rate: loan.interest_rate, instalments: 0, instalment: 3 }); }); it('should consider pay_every', function() { loan.pay_every = 'year'; loan.instalments = 2; var plan = loan.getPaymentPlan(); var date = moment().add(3, 'months').toDate(); loan.amortize(date).should.containDeep(_.omit(plan[0], 'data')); }); it('should calculateMeta when finished', function() { loan.as_of = moment().subtract(3, 'months').toDate(); loan.data.should_amortize = true; var plan = loan.getPaymentPlan(); loan.amortize().should.containDeep({data: {should_amortize: false}}); }); }); }); describe('round', function() { it('should pass the current object through Loan.Format.round', function() { loan = new Loan(fixtures.unrounded); loan.round(0).should.containDeep(fixtures.rounded); }); it('should return a copy', function() { loan = new Loan({invoice_fee: 4.9}); var rounded = loan.round(1); loan.invoice_fee.should.equal(4.9); }); }); describe('toObject', function() { beforeEach(function() { loan = new Loan(fixtures.long); }); it('should return a superficial clone of the loan', function() { var clone = loan.toObject(); clone.should.containDeep(loan); clone.should.not.equal(loan); clone.should.not.be.instanceof(Loan); }); it('should also copy deep objects', function() { loan.data = {description: 'Hello'}; var clone = loan.toObject(); clone.data.should.containDeep(loan.data); loan.data.should.not.equal(clone.data); }); it('should not copy instance functions', function() { loan.toObject().should.not.have.ownProperty('getEffectiveInterestRate'); }); }); describe('toJSON', function() { beforeEach(function() { loan = new Loan(fixtures.long); }); it('should pass to Format.minimal but not return an instance of Loan', function() { var minimal = Loan.Format.minimal(loan); loan.toJSON().should.containDeep(minimal); loan.toJSON().should.not.be.instanceof(Loan); }); }); describe('canCalculateUnknown', function() { it('should return false for invalid arguments', function() { loan.canCalculateUnknown('asdf').should.equal(false); loan.canCalculateUnknown().should.equal(false); }); describe('for instalments', function() { it('should return false when instalments is known', function() { loan.principal = 10000; loan.interest_rate = 0.05; loan.data.monthly_cost = 3000; loan.instalments = 120; loan.canCalculateUnknown('instalments').should.equal(false); }); it('should return true when principal, interest_rate and monthly_cost is known', function() { loan.principal = 10000; loan.interest_rate = 0.05; loan.data.monthly_cost = 3000; loan.canCalculateUnknown('instalments').should.equal(true); }); it('should return false otherwise', function() { loan.principal = 10000; loan.canCalculateUnknown('instalments').should.equal(false); loan.interest_rate = 0.05; loan.canCalculateUnknown('instalments').should.equal(false); delete loan.principal; loan.data.monthly_cost = 3000; loan.canCalculateUnknown('instalments').should.equal(false); }); }); }); describe('calculateUnknown', function() { it('should return an empty object for invalid arguments', function() { loan.calculateUnknown('asdf').should.eql({}); loan.calculateUnknown().should.eql({}); }); describe('for instalments', function() { beforeEach(function() { loan.principal = 10000; loan.interest_rate = 0.05; loan.data.monthly_cost = 3000; }); it('should return an empty object when it is not possible to calculate', function() { (new Loan()).calculateUnknown('instalments').should.eql({}); }); it('should successfully calculate the number of instalments', function() { loan.principal = 10000; loan.interest_rate = 0.05; loan.data.monthly_cost = 3000; _.extend(loan, loan.calculateUnknown('instalments')); delete loan.data.monthly_cost; loan.calculateMeta(); loan.data.monthly_cost.should.be.approximately(3000, 1); }); it('should return 0 when the inputs cant provide a real value', function() { loan.principal = 10000; loan.interest_rate = 0.05; loan.data.monthly_cost = 0; // will result in NaN for the equation loan.calculateUnknown('instalments').should.have.property('instalments', 0); }); }); }); }); });