==========================
== Zhuo Hong Wei's Blog ==
==========================
Any and everything

Mortgage Calculator API

I set myself a challenge to implement an API endpoint in Common Lisp, in hope of putting to use a few things that I have learnt recently,

  • defining structures
  • creating a new project using cl-project:make-project
  • adding and pulling in dependencies
  • serving REST endpoints using hunchentoot web server library
  • using rove testing framework to test drive implementation

And so, I settled on coding up a mortgage calculator which takes in principal (loan amount), tenure (in years) and annual interest rate, and returns a list of monthly payments, each comprising of interest, principal repayment, and outstanding principal so far.

(defstruct mortgage-payment interest principal-repayment outstanding-principal)

(defun round-to (number precision &optional (what #'round))
    (let ((div (expt 10 precision)))
         (/ (funcall what (* number div)) div)))

(defun get-monthly-interest-rate (annual-interest-rate)
  (/ annual-interest-rate 12))

(defun get-number-of-months (years)
  (* years 12))

(defun calculate-monthly-payment (principal tenure annual-interest-rate)
  (let* ((r (get-monthly-interest-rate annual-interest-rate))
         (n (get-number-of-months tenure))
         (x (expt (+ r 1) n)))
         (round-to (/ (* principal r x) (- x 1)) 2)))

(defun generate-payment-schedule (principal tenure annual-interest-rate)
  (let* ((monthly-payment (calculate-monthly-payment principal tenure annual-interest-rate))
         (monthly-interest-rate (get-monthly-interest-rate annual-interest-rate))
         (outstanding-principal principal)
         (schedule))
        (dotimes (n (get-number-of-months tenure))
            (let* ((interest (round-to (* monthly-interest-rate outstanding-principal) 2))
                  (principal-repayment (- monthly-payment interest)))
                  (decf outstanding-principal principal-repayment)
                  (setq schedule 
                    (cons (make-mortgage-payment 
                      :interest interest 
                      :principal-repayment principal-repayment 
                      :outstanding-principal outstanding-principal) schedule))))
        (values 
          monthly-payment
          (nreverse schedule))))

(defun encode-payment-to-json (payment)
  (list 
    (cons 'interest (mortgage-payment-interest payment))
    (cons 'principal-repayment (mortgage-payment-principal-repayment payment))
    (cons 'outstanding-principal (mortgage-payment-outstanding-principal payment))))

(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 3000))

(defun start-mortgage-server ()
    (hunchentoot:define-easy-handler (mortage :uri "/mortgage") (principal years rate)
      (setf (hunchentoot:content-type*) "text/plain")
      (let ((outstanding-principal (parse-float principal :junk-allowed t))
            (tenure (parse-integer years :junk-allowed t))
            (annual-interest-rate (parse-float rate :junk-allowed t)))
        (if (and outstanding-principal tenure annual-interest-rate)
          (multiple-value-bind 
            (monthly-payment schedule)
            (generate-payment-schedule outstanding-principal tenure annual-interest-rate)
            (declare (ignorable monthly-payment))
            (json:encode-json-to-string
              (map 'list #'encode-payment-to-json schedule)))
          (json:encode-json-to-string (list (cons 'error "invalid parameters")))
        )))
    (hunchentoot:start *server*))

Running the server and trying out in the browser,

mortage payments from api

Notice how interest decreases over time as outstanding principal shrinks. Principal repayment correspondingly increases over time as less of each monthly payment count towards interest.

And some basic error handling if query parameters fail to parse,

error from api