cfpb/owning-a-home

View on GitHub
src/static/js/modules/formalize.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

var $ = require( 'jquery' );
var debounce = require( 'debounce' );
var cost = require( 'overall-loan-cost' );
var objectify = require( 'objectified' );
var formatUSD = require( 'format-usd' );
var positive = require( 'stay-positive' );
var amortize = require( 'amortize' );
var humanizeLoanType = require( './humanize-loan-type' );
var supportsAccessors = require( './supports-accessors' );
var fetchRates = require( './rates' );


var loans = {};
var editableFields = [
  'location',
  'minfico',
  'maxfico',
  'amount-borrowed',
  'price',
  'discount',
  'down-payment',
  'rate-structure',
  'loan-term',
  'loan-type',
  'arm-type'
];

function createNewForm( id, loanData ) {

  var loan = objectify( '#lc-input-' + id, [
    {
      name:   'amount-borrowed',
      source: 'house-price-input - down-payment-input'
    }, {
      name:   'price',
      source: 'house-price-input'
    }, {
      name:   'down-payment',
      source: 'down-payment-input'
    }, {
      name:   'rate-structure',
      source: 'rate-structure-select'
    }, {
      name:   'loan-term',
      source: 'loan-term-select'
    }, {
      name:   'loan-type',
      source: 'loan-type-select'
    }, {
      name:   'arm-type',
      source: function() {
        return $( '#arm-type-select-' + id ).val();
      }
    }, {
      name:   'interest-rate',
      source: 'interest-rate-select'
    }, {
      name:   'raw-discount',
      source: function() {
        return $( '#points-' + id + ' input:checked' ).val();
      }
    }, {
      name:   'discount',
      source: function() {
        var points = loan['raw-discount'] / 100;
        return points * loan['amount-borrowed'];
      }
    }, {
      name:   'processing',
      source: function() {
        return loan['amount-borrowed'] / 100;
      }
    }, {
      name:   'third-party-services',
      source: function() {
        return 3000;
      }
    }, {
      name:   'mortgage-insurance',
      source: function() {
        return 0;
      }
    }, {
      name:   'taxes-gov-fees',
      source: function() {
        return 1000;
      }
    }, {
      name:   'prepaid-expenses',
      source: function() {
        return 500;
      }
    }, {
      name:   'initial-escrow',
      source: function() {
        return 500;
      }
    }, {
      name:   'monthly-taxes-insurance',
      source: function() {
        var yearly = loan.price / 100,
            propertyTaxes = yearly / 12,
            homeInsurance = ( 0.05 * loan.price ) / 12;
        return propertyTaxes + homeInsurance;
      }
    }, {
      name:   'monthly-hoa-dues',
      source: function() {
        return 0;
      }
    }, {
      name:   'monthly-principal-interest',
      source: function() {
        return amortize( {
          amount:       positive( loan['amount-borrowed'] ),
          rate:         loan['interest-rate'],
          totalTerm:    loan['loan-term'] * 12,
          amortizeTerm: 60 // @todo loan term * 12?
        } ).payment;
      }
    }, {
      name:   'monthly-mortgage-insurance',
      source: function() {
        return 0;
      }
    }, {
      name:   'closing-costs',
      source: function() {
        return loan['down-payment'] +
               loan.discount +
               loan.processing +
               loan['third-party-services'] +
               loan['mortgage-insurance'] +
               loan['taxes-gov-fees'] +
               loan['prepaid-expenses'] +
               loan['initial-escrow'];
      }
    }, {
      name:   'monthly-payment',
      source: function() {
        var taxes = loan['monthly-taxes-insurance'],
            insurance = loan['monthly-mortgage-insurance'],
            hoa = loan['monthly-hoa-dues'],
            monthlyPrincipalInterest = loan['monthly-principal-interest'];
        return taxes + insurance + hoa + monthlyPrincipalInterest;
      }
    }, {
      name:   'principal-paid',
      source: function() {
        return cost( {
          amountBorrowed: positive( loan['amount-borrowed'] ),
          rate: loan['interest-rate'],
          totalTerm: loan['loan-term'] * 12,
          downPayment: loan['down-payment'],
          closingCosts: loan['closing-costs']
        } ).totalEquity;
      }
    }, {
      name:   'interest-fees-paid',
      source: function() {
        return cost( {
          amountBorrowed: positive( loan['amount-borrowed'] ),
          rate: loan['interest-rate'],
          totalTerm: loan['loan-term'] * 12,
          downPayment: loan['down-payment'],
          closingCosts: loan['closing-costs']
        } ).totalCost;
      }
    }, {
      name:   'overall-cost',
      source: function() {
        return cost( {
          amountBorrowed: positive( loan['amount-borrowed'] ),
          rate: loan['interest-rate'],
          totalTerm: loan['loan-term'] * 12,
          downPayment: loan['down-payment'],
          closingCosts: loan['closing-costs']
        } ).overallCost;
      }
    }
  ] );

  // Cache these for later
  var $form = $( '#lc-input-' + id );
  var $rateSelect = $( '#interest-rate-select-' + id );
  var $amount = $( '.loan-amount-display-' + id );
  var $closing = $( '.closing-costs-display-' + id );
  var $downPayment = $( '.down-payment-display-' + id );
  var $lenderFees = $( '.lender-fees-display-' + id );
  var $discountAmount = $( '.discount-display-' + id );
  var $processing = $( '.processing-fees-display-' + id );
  var $thirdPartyFees = $( '.third-party-fees-display-' + id );
  var $thirdPartyServices = $( '.third-party-services-display-' + id );
  var $mortgageInsurance = $( '.mortgage-insurance-display-' + id );
  var $taxesGovFees = $( '.taxes-gov-fees-display-' + id );
  var $prepaid = $( '.prepaid-expenses-display-' + id );
  var $initialEscrow = $( '.initial-escrow-display-' + id );
  var $monthlyPrincipalInterest = $( '.monthly-principal-interest-display-' + id );
  var $monthlyMortgageInsurance = $( '.monthly-mortgage-insurance-display-' + id );
  var $monthlyTaxes = $( '.monthly-taxes-insurance-display-' + id );
  var $monthlyHOA = $( '.monthly-hoa-dues-display-' + id );
  var $monthly = $( '.monthly-payment-display-' + id );
  var $loanTerm = $( '.loan-term-display-' + id );
  var $principalPaid = $( '.principal-paid-display-' + id );
  var $interestPaid = $( '.interest-fees-paid-display-' + id );
  var $overall = $( '.overall-costs-display-' + id );
  var $interest = $( '.interest-rate-display-' + id );
  var $percent = $( '#percent-dp-input-' + id );
  var $down = $( '#down-payment-input-' + id );
  var $discount = $( '.discount-' + id );
  var $summaryYear = $( '.lc-summary-year-' + id );
  var $summaryStruct = $( '.lc-summary-structure-' + id );
  var $summaryType = $( '.lc-summary-type-' + id );

  // Keep track of the last down payment field that was accessed.
  var percentDownAccessedLast;

  // Keep track of any api rate request in progress
  var currentRequest;

  function updateComparisons( changes ) {
    var loanDataChanged = false;

    for ( var i = 0, len = changes.length; i < len; i++ ) {
      if ( changes[i].name === 'down-payment' && typeof percentDownAccessedLast !== 'undefined' && !percentDownAccessedLast ) {
        var val = loan['down-payment'] / loan.price * 100;
        $percent.val( Math.round( val ) );
        percentDownAccessedLast = false;
      }
      // If a user-editable field has changed, rate needs updating
      if ( !loanDataChanged && ( $.inArray( changes[i].name, editableFields ) !== -1 ) ) {
        loanDataChanged = true;
      }
    }

    if ( loanDataChanged ) {
      if ( currentRequest ) {
        getRateData();
      } else {
        $form.removeClass( 'updating' ).addClass( 'update' );
      }
    }

    updateLoanDisplay();
  }

  function updateLoanDisplay() {
    $amount.text( formatUSD( positive( loan['amount-borrowed'] ), { decimalPlaces: 0 } ) );
    $closing.text( formatUSD( loan['closing-costs'], { decimalPlaces: 0 } ) );
    $downPayment.text( formatUSD( loan['down-payment'], { decimalPlaces: 0 } ) );
    $lenderFees.text( formatUSD( loan.discount + loan.processing, { decimalPlaces: 0 } ) );
    $discountAmount.text( formatUSD( loan.discount, { decimalPlaces: 0 } ) );
    $processing.text( formatUSD( loan.processing, { decimalPlaces: 0 } ) );
    $thirdPartyFees.text( formatUSD( loan['third-party-services'] + loan['mortgage-insurance'], { decimalPlaces: 0 } ) );
    $thirdPartyServices.text( formatUSD( loan['third-party-services'], { decimalPlaces: 0 } ) );
    $mortgageInsurance.text( formatUSD( loan['mortgage-insurance'], { decimalPlaces: 0 } ) );
    $taxesGovFees.text( formatUSD( loan['taxes-gov-fees'], { decimalPlaces: 0 } ) );
    $prepaid.text( formatUSD( loan['prepaid-expenses'], { decimalPlaces: 0 } ) );
    $initialEscrow.text( formatUSD( loan['initial-escrow'], { decimalPlaces: 0 } ) );
    $monthlyPrincipalInterest.text( formatUSD( loan['monthly-principal-interest'], { decimalPlaces: 0 } ) );
    $monthlyMortgageInsurance.text( formatUSD( loan['monthly-mortgage-insurance'], { decimalPlaces: 0 } ) );
    $monthlyTaxes.text( formatUSD( loan['monthly-taxes-insurance'], { decimalPlaces: 0 } ) );
    $monthlyHOA.text( formatUSD( loan['monthly-hoa-dues'], { decimalPlaces: 0 } ) );
    $monthly.text( formatUSD( loan['monthly-payment'], { decimalPlaces: 0 } ) );
    $loanTerm.text( 'Costs at ' + loan['loan-term'] + ' years' );
    $principalPaid.text( formatUSD( loan['principal-paid'], { decimalPlaces: 0 } ) );
    $interestPaid.text( formatUSD( loan['interest-fees-paid'], { decimalPlaces: 0 } ) );
    $overall.text( formatUSD( loan['overall-cost'], { decimalPlaces: 0 } ) );
    $interest.text( loan['interest-rate'] );
    $discount.text( loan['raw-discount'] );
    $summaryYear.text( loan['loan-term'] );
    $summaryStruct.text( loan['rate-structure'] );
    $summaryType.text( humanizeLoanType( loan['loan-type'] ) );
  }

  function getRateData() {
    if ( currentRequest && typeof currentRequest === 'object' ) {
      currentRequest.abort();
    }

    $form.removeClass( 'update' ).addClass( 'updating' );

    currentRequest = fetchRates( {
      price:          loan.price,
      loan_amount:    loan['amount-borrowed'],
      minfico:        loan.minfico,
      maxfico:        loan.maxfico,
      state:          loan.location,
      rate_structure: loan['rate-structure'],
      loan_term:      loan['loan-term'],
      loan_type:      loan['loan-type'],
      arm_type:       loan['arm-type'],
      points:         loan['raw-discount']
    } )
    .done( function( results ) {
      currentRequest = null;
      var rates = [];
      for ( var key in results.data ) {
        if ( results.data.hasOwnProperty( key ) ) {
          rates.push( key );
        }
      }
      rates.sort();
      updateRateSelect( rates );
      loan.update();
      $form.removeClass( 'updating' );
    } ).fail( function() {
      currentRequest = null;
      $form.removeClass( 'updating' )
        .addClass( 'update' );
    } );
  }

  function updateRateSelect( rates ) {
    rates = rates || [];
    var len = rates.length;
    var half = Math.floor( ( len - 1 ) / 2 );

    $rateSelect.empty();
    $.each( rates, function( ind, rate ) {
      var opt = $( '<option></option>' )
                  .attr( 'value', rate )
                  .text( rate + '%' );
      if ( ind === half ) {
        opt.attr( 'selected', 'selected' );
      }
      $rateSelect.append( opt );
    } );
  }

  function _updateDownPayment( ev ) {
    var targetID = ev.target.id;
    var val;

    if ( /percent/.test( targetID ) ) {
      val = $percent.val() / 100 * loan.price;
      $down.val( Math.round( val ) );
      percentDownAccessedLast = true;
      loan.update();
      return;
    }

    if ( /down\-payment/.test( targetID ) ) {
      percentDownAccessedLast = false;
    }

    if ( /house\-price/.test( targetID ) &&
         typeof percentDownAccessedLast !== 'undefined' ) {
      if ( percentDownAccessedLast ) {
        val = $percent.val() / 100 * loan.price || 0;
        $down.val( Math.round( val ) );
        loan.update();
      } else {
        val = loan['down-payment'] / loan.price * 100 || 0;
        $percent.val( Math.round( val ) );
        loan.update();
      }
    }
  }

  // The pricing fields (price, dp, dp %) are wonky and require special handling.
  $( '#lc-input-' + id ).on( 'keyup', '.pricing input', debounce( _updateDownPayment, 500 ) );

  // update when the radio buttons are updated
  // todo: there's certainly a cleaner way to do this
  $( '#points-' + id ).on( 'click', 'input', function updatePoints() {
    loan.update();
  } );

  // refresh interest rates when update button is clicked
  $( '#interest-rate-update-' + id ).click( function updateRates( evt ) {
    evt.preventDefault();
    getRateData();
  } );

  function init() {
    // update the loan object with values from form
    loan.update();
    $.extend( loan, loanData );
    loans[id] = loan;

    // make initial api request for rates
    getRateData();

    // Observe the loan object for changes *only* if the browser supports it.
    // If the browser doesn't support it, do some drrrrty checking.
    if ( supportsAccessors ) {
      Object.observe( loan, updateComparisons );
    } else {
      var oldLoan = $.extend( {}, loan );
      setInterval( function() {
        // TODO: fix this
        if ( JSON.stringify( loan ) !== JSON.stringify( oldLoan ) ) {
          updateComparisons( [] );
          oldLoan = $.extend( {}, loan );
        }
      }, 500 );
    }
  }

  init();

  return loans;
}

module.exports = createNewForm;