raif.lisp 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. (uiop:define-package :enikesha-scripts/raif
  2. (:use :cl)
  3. (:mix :uiop :drakma :cl-ppcre :cxml-stp)
  4. (:export
  5. #:main))
  6. (in-package :enikesha-scripts/raif)
  7. (defvar *raif-ws* "https://connect.raiffeisen.ru/Mobile-WS/services/" "Base WS URL")
  8. (defvar *ua* "Dalvik/2.1.0 (Linux; U; Android 6.0.1; ONE A2003 Build/MMB29M) Android/3.19.0(459)" "User agent")
  9. (defvar *cookie-jar* (make-instance 'drakma:cookie-jar) "Default cookie jar")
  10. (defvar *soap-dom* (cxml:parse "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><soapenv:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://entry.rconnect/xsd\" xmlns:ser=\"http://service.rconnect\" xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\"><soapenv:Header /><soapenv:Body></soapenv:Body></soapenv:Envelope>" (stp:make-builder)) "XML SOAP raif service envelope")
  11. (defparameter +ns-soap-env+ "http://schemas.xmlsoap.org/soap/envelope/" "Soap envelope xmlns")
  12. (defparameter +ns-rconnect+ "http://service.rconnect" "Service r-connect xmlns")
  13. (defparameter +ct-xml+ "text/xml;charset=UTF-8" "XML Content-Type")
  14. (defvar *rc-auth-service* "RCAuthorizationService")
  15. (defvar *rc-account-service* "RCAccountService")
  16. (defun reset-cookie ()
  17. (setf *cookie-jar* (make-instance 'drakma:cookie-jar)))
  18. (defun get-service-url (service-name)
  19. (concatenate 'string *raif-ws* service-name))
  20. (defun soap-request-serialize (body)
  21. (let* ((doc (stp:copy *soap-dom*))
  22. (n-body (stp:find-recursively-if (stp:of-name "Body" +ns-soap-env+) doc)))
  23. (stp:append-child n-body body)
  24. (stp:serialize doc (cxml:make-string-sink))))
  25. (defun %rs/login (login password)
  26. (let ((n-method (stp:make-element "ser:login" +ns-rconnect+))
  27. (n-login (stp:make-element "login"))
  28. (n-password (stp:make-element "password")))
  29. (stp:append-child n-login (stp:make-text login))
  30. (stp:append-child n-password (stp:make-text password))
  31. (stp:append-child n-method n-login)
  32. (stp:append-child n-method n-password)
  33. n-method))
  34. (defun %r-call (service request)
  35. (multiple-value-bind (body code)
  36. (drakma:http-request (get-service-url service)
  37. :method :post
  38. :content (soap-request-serialize request)
  39. :content-type +ct-xml+
  40. :cookie-jar *cookie-jar*
  41. :user-agent *ua*
  42. :external-format-in :utf-8
  43. :external-format-out :utf-8
  44. :additional-headers `(("SOAPAction" . "")))
  45. (values (stp:first-child
  46. (stp:find-recursively-if
  47. (stp:of-name "Body" +ns-soap-env+)
  48. (cxml:parse body (stp:make-builder))))
  49. code)))
  50. (defun rc/perform-login (login password)
  51. (reset-cookie)
  52. (multiple-value-bind (info code)
  53. (%r-call *rc-auth-service* (%rs/login login password))
  54. (if (= code 200)
  55. info
  56. (error "Login error"))))
  57. (defun rc/get-batch-response ()
  58. (multiple-value-bind (res code)
  59. (%r-call *rc-auth-service* (make-element "ser:getBatchResponse" +ns-rconnect+))
  60. (when (= code 200)
  61. res)))
  62. (defun get-accounts (batch-response)
  63. (stp:filter-recursively (stp:of-name "accounts") batch-response))
  64. (defun %rs/get-last-account-movements (account count)
  65. (let ((n-method (stp:make-element "ser:GetLastAccountMovements" +ns-rconnect+))
  66. (n-account (stp:copy account))
  67. (n-count (stp:make-element "count")))
  68. (setf (stp:local-name n-account) "account")
  69. (stp:append-child n-count (stp:make-text (princ-to-string count)))
  70. (stp:append-child n-method n-account)
  71. (stp:append-child n-method n-count)
  72. n-method))
  73. (defun rc/get-last-account-movements (account count)
  74. (multiple-value-bind (info code)
  75. (%r-call *rc-account-service* (%rs/get-last-account-movements account count))
  76. (when (= code 200)
  77. (stp:filter-recursively (stp:of-name "return") info))))
  78. (defun %rs/get-account-movements (account start-date end-date)
  79. (let ((n-method (stp:make-element "ser:GetAccountMovements" +ns-rconnect+))
  80. (n-account (stp:copy account))
  81. (n-start-date (stp:make-element "startDate"))
  82. (n-end-date (stp:make-element "endDate")))
  83. (setf (stp:local-name n-account) "account")
  84. (stp:append-child n-start-date (stp:make-text start-date))
  85. (stp:append-child n-end-date (stp:make-text end-date))
  86. (stp:append-child n-method n-account)
  87. (stp:append-child n-method n-start-date)
  88. (stp:append-child n-method n-end-date)
  89. n-method))
  90. (defun rc/get-account-movements (account start-ut end-ut)
  91. (labels ((format-ut (ut)
  92. (multiple-value-bind (sec min hour day month year)
  93. (decode-universal-time ut)
  94. (format nil "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D.0"
  95. year month day hour min sec))))
  96. (multiple-value-bind (info code)
  97. (%r-call *rc-account-service*
  98. (%rs/get-account-movements account
  99. (format-ut start-ut)
  100. (format-ut end-ut)))
  101. (when (= code 200)
  102. (stp:filter-recursively (stp:of-name "return") info)))))
  103. (defun tag->keyword (tag)
  104. (setf tag (cl-ppcre:regex-replace-all "([a-z])([A-Z])" tag '(0 "-" 1)))
  105. (setf tag (cl-ppcre:regex-replace-all "_" tag "-"))
  106. (intern (string-upcase tag) :keyword))
  107. (defun node->alist (node)
  108. (let (res)
  109. (stp:do-children (child node (nreverse res))
  110. (push (cons (tag->keyword (stp:local-name child))
  111. (stp:string-value child))
  112. res))))
  113. (defun aget (alist &rest keys)
  114. (loop for key in keys
  115. for ret = (cdr (assoc key alist :test #'equal)) then (cdr (assoc key ret :test #'equal))
  116. finally (return ret)))
  117. (defun get-account-currency (account)
  118. (let ((currency (aget account :currency)))
  119. (cond
  120. ((equal currency "RUR") "RUB")
  121. (t currency))))
  122. (defun get-account-name (account)
  123. (let ((num (aget account :number)))
  124. (concatenate 'string "assets:Raiffeisen:"
  125. (cond
  126. ((string= num "40817810503000266700") "Debit")
  127. ((string= num "40817978803000110883") "Savings:EUR")
  128. ((string= num "40817810203001534278") "Savings:Renov")
  129. ((string= num "40817810103001667025") "Savings:For credit")
  130. (:overwise (concatenate 'string
  131. (aget account :account-type)
  132. ":"
  133. num
  134. ":"
  135. (get-account-currency account)))))))
  136. (defun get-move-payee (move)
  137. (aget move :short-description))
  138. (defun get-move-description (move)
  139. (let ((full (aget move :full-description)))
  140. (unless (or (zerop (length full))
  141. (string= "CARD **" (subseq full 0 (min (length full) 7))))
  142. (cl-ppcre:regex-replace-all "\\s" full " "))))
  143. (defun get-move-account2 (move)
  144. (let ((desc (aget move :short-description)))
  145. (cond
  146. ((equal desc "P/O 001,40817810503000266700RC") "assets:Raiffeisen:Debit")
  147. ((equal desc "P/O 001,40817810203001534278RC") "assets:Raiffeisen:Savings:Renov")
  148. ((equal desc "TINKOFF BANK CARD2CARD MOSCOW") "assets:Tinkoff:Debit")
  149. ((equal (subseq desc 0 (min 7 (length desc))) "RBA ATM") "assets:Cash:RUB")
  150. (t (if (string= "1" (aget move :type)) "income" "expenses")))))
  151. (defun short-date (date)
  152. (cl-ppcre:regex-replace-all "-" (subseq date 0 10) "/"))
  153. (defun moves->journal (account moves)
  154. (let ((account1 (get-account-name account))
  155. (currency (get-account-currency account))
  156. entries)
  157. (loop for move in moves
  158. for date = (short-date (aget move :commit-date))
  159. for payee = (get-move-payee move)
  160. for description = (get-move-description move)
  161. for id = (aget move :id)
  162. for amount = (read-from-string (aget move :amount))
  163. for account2 = (get-move-account2 move)
  164. for expense = (equal "0" (aget move :type))
  165. do (push (format nil "~A~@[ (~A)~] ~A~@[ ; ~A~]~% ~37A ~A~,2F ~A~% ~37A ~A~,2F ~A"
  166. date nil payee description
  167. account1 (if expense "-" " ") amount currency
  168. account2 (if expense " " "-") amount currency)
  169. entries))
  170. ;; Total account balance
  171. (push (format nil "; balance ~A ~A = ~,2F ~A"
  172. (short-date (aget account :balance-date))
  173. account1
  174. (read-from-string (aget account :balance))
  175. (get-account-currency account))
  176. entries)
  177. (nreverse entries)))
  178. (defun main (argv)
  179. (destructuring-bind (login pass type) argv
  180. (rc/perform-login login pass)
  181. (let* ((accounts (get-accounts (rc/get-batch-response)))
  182. (end (get-universal-time))
  183. (start (- end (* 60 60 24 (cond
  184. ((equal type "week") 7)
  185. ((equal type "day") 1)
  186. ((equal type "month") 30)
  187. (t 7)))))
  188. (moves (mapcar (lambda (a) (cons (node->alist a)
  189. (mapcar #'node->alist
  190. (if (string= type "last")
  191. (nreverse (rc/get-last-account-movements a 10))
  192. (rc/get-account-movements a start end)))))
  193. accounts)))
  194. (format t "~{~A~^~%~%~}~%" (loop for (a . m) in moves append (moves->journal a m))))))