git.fiddlerwoaroof.com
freshbooks.lisp
d2878c38
 (defpackage #:tempores.freshbooks
870680c2
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
d2878c38
         #:tempores.parser)
   (:import-from #:tempores #:tempores)
   (:export #:tempores))
870680c2
 
d2878c38
 (in-package :tempores.freshbooks)
0bd25181
 
 (defvar *api-key*)
 (defvar *endpoint*)
 
 (defun init ()
d2878c38
   (ubiquitous:restore 'tempores)
0bd25181
   (ubiquitous:value :freshbooks :api-key)
   (ubiquitous:value :freshbooks :endpoint))
 
4f2543e4
 (eval-when (:load-toplevel :compile-toplevel :execute)
   (xhtmlambda::def-element <::request) 
   (xhtmlambda::def-element <::time_entry)
   (xhtmlambda::def-element <::project_id)
   (xhtmlambda::def-element <::task_id)
   (xhtmlambda::def-element <::hours)
   (xhtmlambda::def-element <::notes)
   (xhtmlambda::def-element <::date))
0bd25181
 
 (defun post-to-endpoint (xml)
   (let ((drakma:*text-content-types* (acons "application" "xml" drakma:*text-content-types*)))
     (drakma:http-request *endpoint*
                          :basic-authorization (list *api-key* "X")
                          :method :post
                          :content (with-output-to-string (s)
                                     (format s "~w" xml)))))
 
 (defun parsed-api-call (xml)
af9f5f80
   (plump:parse (post-to-endpoint xml)))
0bd25181
 
 (defmacro define-api-call (name (&rest args) method-name &body elements)
   `(defun ,name ,args
      (parsed-api-call (<:request (:method ,method-name) ,@elements))))
 
51faefd0
 (define-api-call create-time-entry (entry-spec) "time_entry.create"
   entry-spec)
 
0bd25181
 (define-api-call list-invoices () "invoice.list")
 (define-api-call list-projects () "project.list")
 (define-api-call list-tasks () "task.list")
 (define-api-call list-payments () "payment.list")
 (define-api-call list-clients () "client.list")
 
 (defgeneric slots-for (cls)
   (:method-combination append))
 
 (defmacro define-simple-class (name (&rest supers) &body elements)
   (let ((schema-name (intern
                        (string-join (list name "-SCHEMA"))))
         (schema-supers (loop for super in supers
                              collect (intern
                                        (string-join
                                          (list (symbol-name super)
                                                "-schema"))))))
34aff00c
     `(eval-when (:compile-toplevel :load-toplevel :execute)
        (prog1
          (defclass ,name ,supers
            ,(list*
               '(registry :initform (make-hash-table :test 'equal) :allocation :class)
               (loop for element in elements
                     collect `(,element :initarg ,(make-keyword element) :initform nil))))
          (defclass ,schema-name ,schema-supers ())
          (defmethod slots-for append ((cls ,schema-name))
            ',elements)))))
0bd25181
 
 (define-simple-class task ()
   task_id name description billable rate)
 
 (define-simple-class project ()
   project_id name description rate bill_method client_id hour_budget
   tasks staff)
 
d2878c38
 (tempores.macros:define-printer (task s)
1b0fea02
   ((with-slots (task_id name) task
     (format s "~i~a (~a):" name task_id)))
   ((with-slots (task_id name) task
     (format s "~a (~a)" name task_id))))
0bd25181
 
d2878c38
 (tempores.macros:define-printer (project s)
1b0fea02
   ((with-slots (project_id name tasks) project
     (format s "~i~a (~a):~%~{~a~%~}" name project_id tasks)))
   ((with-slots (project_id name tasks) project
     (format s "~a (~a): ~a tasks" name project_id (length tasks)))))
0bd25181
 
 (defparameter *task-registry* (make-hash-table :test 'equal))
 (defmethod initialize-instance :after ((self task) &key &allow-other-keys)
   (loop for slot in '(task_id name description billable rate)
         for node = (slot-value self slot)
         unless (null node) do (setf (slot-value self slot) (plump:text node))))
 
 (defun register-task (task)
   (with-slots (task_id) task
     (setf (gethash task_id *task-registry*) task)))
 
 (defparameter *project-registry* (make-hash-table :test 'equal))
 (defmethod initialize-instance :after ((self project) &key &allow-other-keys)
   (loop for slot in '(project_id name description rate bill_method client_id hour_budget)
         for node = (slot-value self slot)
         unless (null node) do (setf (slot-value self slot) (plump:text node)))
 
   (with-slots (tasks) self
     (setf tasks
           (loop for task across (lquery:$ (inline tasks)  "> task")
                 for task_id = (lquery:$1 (inline task) "task_id" (text))
                 for task-obj = (gethash task_id *task-registry*)
                 unless task-obj do (setf task-obj (parse-task task))
                 collect task-obj))))
 
 (defun register-project (project)
   (with-slots (name) project
     (setf (gethash name *project-registry*) project)))
 
 
 
 (defun parse-task (parsed-xml)
   (apply #'make-instance 'task
          (loop for slot in (slots-for (make-instance 'task-schema))
                nconc (let ((slot-name (format nil "> ~(~a~)" slot)))
                        (list (make-keyword slot)
                              (lquery:$ (inline parsed-xml)
                                        slot-name
                                        (node)))))))
 
 (defun parse-project (parsed-xml)
   (apply #'make-instance 'project
51faefd0
          (loop for slot in (slots-for (make-instance 'project-schema))
                nconc (let ((slot-name (format nil "> ~(~a~)" slot)))
                        (list (make-keyword slot)
                              (lquery:$ (inline parsed-xml)
                                        slot-name
                                        (node)))))))
0bd25181
 
 (defun get-project (name)
   (let ((projects (sort (map 'vector
                              (alambda (cons (string-downcase it) it))
                              (hash-table-keys *project-registry*))
                         #'string<
                         :key #'car))
         (name (string-downcase name)))
     (gethash
       (cdr
         (find-if (alambda (string= it name :end1 (length name)))
                  projects
                  :key #'car))
       *project-registry*)))
 
 
 (defun make-time-entry (project task date hours notes)
   (<:time_entry ()
                 (<:project_id ()
74adbb8b
                               (parse-integer
                                 (slot-value (get-project project)
                                             'project_id)))
                 (<:task_id () (parse-integer task))
0bd25181
                 (<:date () (identity date))
                 (<:hours () (identity hours))
                 (<:notes () (identity notes))))
 
 (defun get-task-by-name (name)
   (let ((tasks (mapcar
                  (destructuring-lambda ((id . task))
                    (cons (slot-value task 'name)
                          id))
                  (hash-table-alist *task-registry*))))
     (cdr (assoc name tasks :test #'string-equal))))
 
d2878c38
 (defun tempores-to-entries (tempores-log)
0bd25181
   (let ((task-id (get-task-by-name "General")))
d2878c38
     (loop for entry in tempores-log
           for date = (tempores::date entry)
           for project = (tempores::client entry)
           for note = (tempores::memo entry)
           for hours = (tempores::duration entry)
74adbb8b
           for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d 00:00:00~}"
d2878c38
                                  (reverse (tempores.cli::unroll-date date)))
0bd25181
           collect (make-time-entry project task-id fmt-date hours note))))
 
 (defun make-entry-updates ()
d2878c38
   (let ((updates (tempores-to-entries (tempores::get-log #p"/home/edwlan/bucket/time.md"))))
0bd25181
     (loop for update in updates
           collect (<:request (:method "time_entry.create") update))))
51faefd0
 
b9ab1c24
 (defun get-entry-data ()
51faefd0
   (init)
b9ab1c24
   (setf *endpoint* (ubiquitous:value :freshbooks :endpoint))
51faefd0
   (setf *api-key* (ubiquitous:value :freshbooks :api-key))
 
   (let-each (:be *)
     (list-tasks)
     (lquery:$ * "task")
     (map 'list #'parse-task *)
     (mapcar #'register-task *))
 
   (let-each (:be *)
     (list-projects)
     (lquery:$ * "project")
     (map 'list #'parse-project *)
     (mapcar #'register-project *))
 
b9ab1c24
   (make-entry-updates))
 
 (defun post-time-entries-main ()
51faefd0
   (let-each (:be *)
b9ab1c24
     (get-entry-data)
51faefd0
     (mapcar #'parsed-api-call *)))