raiffeisen.lisp 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.raiffeisen
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.raiffeisen)
  5. (defsetting *account-aliases* nil "account to ledger account")
  6. (defsetting *account-default-prefix* "assets:Raiffeisen")
  7. (defsetting *account-loan-prefix* "liabilities:Raiffeisen")
  8. (defsetting *move-categories* nil "move categoryId->account")
  9. (defsetting *account-cash* "assets:Cash:RUB")
  10. (defparameter +raif-oauth-url+ "https://online.raiffeisen.ru/oauth/token")
  11. (defparameter +raif-oauth-basic-auth+ '("oauthUser" . "oauthPassword!@"))
  12. (defvar *access-token* nil "Currently active access token")
  13. (defvar *credentials-provider* nil "Active credentials provider")
  14. (defun get-access-token (username password)
  15. (let ((response
  16. (json-request +raif-oauth-url+ :method :post
  17. :content `(("grant_type" . "password")
  18. ("username" . ,username)
  19. ("password" . ,password))
  20. :basic-auth +raif-oauth-basic-auth+)))
  21. (values (agets response "access_token")
  22. (agets response "expires_in")
  23. (agets response "resource_owner"))))
  24. (defun filled (alist)
  25. (remove nil alist :key #'cdr))
  26. (defparameter +raif-rest-url+ "https://online.raiffeisen.ru/rest/")
  27. (defun %rest (method &optional parameters)
  28. (handler-case
  29. (json-request (concatenate 'string +raif-rest-url+ method)
  30. :parameters (filled parameters)
  31. :headers (when *access-token*
  32. `((:authorization . ,(concatenate 'string "Bearer " *access-token*)))))
  33. (dex:http-request-unauthorized (e)
  34. (if *credentials-provider*
  35. (progn
  36. (funcall *credentials-provider*
  37. (lambda (login password)
  38. (setf *access-token* (get-access-token login password))))
  39. (let (*credentials-provider*) ;; Retry call resetting *credentials-provider* to prevent loop
  40. (%rest method parameters)))
  41. (error e)))))
  42. (defun ^request (&key (page 0) (size 20) (sort "date") (order "desc") alien)
  43. (%rest "request" `(("page" . ,page)
  44. ("size" . ,size)
  45. ("sort" . ,sort)
  46. ("order" . ,order)
  47. ("alien" . ,(when alien "true")))))
  48. (defun ^transaction (&key (page 0) (size 20) (sort "date") (order "desc"))
  49. (%rest "transaction" `(("page" . ,page)
  50. ("size" . ,size)
  51. ("sort" . ,sort)
  52. ("order" . ,order))))
  53. (defun ^account (&key alien)
  54. (%rest "account" `(("alien" . ,(when alien "true")))))
  55. (defun ^loan (&key alien)
  56. (%rest "loan" `(("alien" . ,(when alien "true")))))
  57. (defun get-account-currency (account)
  58. (agets account "currency" "shortName"))
  59. (defun get-account-name (account)
  60. (if (agets account "leftDebt")
  61. (or (agets *account-aliases* (agets account "id"))
  62. (format nil "~A:~A:~A:~A"
  63. *account-loan-prefix*
  64. (agets account "type" "id")
  65. (agets account "id")
  66. (get-account-currency account)))
  67. (or (agets *account-aliases* (agets account "cba"))
  68. (when-let (name (agets account "name"))
  69. (format nil "~A:~A" *account-default-prefix* name))
  70. (format nil "~A:~A:~A:~A"
  71. *account-default-prefix*
  72. (agets account "type" "name")
  73. (agets account "cba")
  74. (get-account-currency account)))))
  75. (defun get-transaction-account (transaction accounts)
  76. (let ((relation (agets transaction "relation"))
  77. (related-id (agets transaction "relatedId")))
  78. (cond
  79. ((equal relation "ACCOUNT")
  80. (find related-id accounts :test 'equal :key (agetter "id")))
  81. ((equal relation "CARD")
  82. (some (lambda (acc)
  83. (when (find related-id (agets acc "cards") :test 'equal :key (agetter "id"))
  84. acc))
  85. accounts)))))
  86. (defvar *default-payee* "Raiffeisen")
  87. (defun get-move-payee (move)
  88. (or (agets move "merchant")
  89. *default-payee*))
  90. (defun get-move-description (move)
  91. (agets move "note"))
  92. (defun get-move-account2 (move)
  93. (let ((category (agets move "categoryId"))
  94. (category-id (agets move "parentCategoryId")))
  95. (cond
  96. ((agets *move-categories* category-id))
  97. ((equal category "ATM") *account-cash*)
  98. (t (format nil "~A~@[:~A~]"
  99. (if (> (agets move "amount") 0) "income" "expenses") category)))))
  100. (defun short-date (date)
  101. (cl-ppcre:regex-replace-all "-" (subseq date 0 10) "/"))
  102. (defun move->entry (move)
  103. (let* ((account (agets move "account"))
  104. (account1 (get-account-name account))
  105. (account2 (get-move-account2 move))
  106. (currency (agets move "currency" "shortName"))
  107. (bill-currency (agets move "billCurrency" "shortName"))
  108. (date (pta-ledger:parse-date (short-date (agets move "date"))))
  109. (payee (get-move-payee move))
  110. (description (get-move-description move))
  111. (amount (agets move "amount"))
  112. (bill-amount (agets move "billAmount")))
  113. (pta-ledger:make-entry
  114. :date date
  115. :description payee
  116. :comment description
  117. :postings (list
  118. (pta-ledger:make-posting
  119. :account account2
  120. :amount (pta-ledger:make-amount
  121. :quantity (* amount -1)
  122. :commodity currency)
  123. :total-price (unless (equal currency bill-currency)
  124. (pta-ledger:make-amount
  125. :quantity (* bill-amount -1)
  126. :commodity bill-currency)))
  127. (pta-ledger:make-posting
  128. :account account1
  129. :amount (pta-ledger:make-amount
  130. :quantity bill-amount
  131. :commodity bill-currency))))))
  132. (defun account->balance (account)
  133. (let ((left-debt (agets account "leftDebt")))
  134. (format nil "; balance ~A = ~,2F ~A"
  135. (get-account-name account)
  136. (if left-debt (* -1 left-debt) (agets account "balance"))
  137. (get-account-currency account))))
  138. (defun format-entries (changes)
  139. (text-chunks (mapcar #'pta-ledger:render changes)))
  140. (defun format-balance (accounts)
  141. (format nil "```~%~{~A~^~%~}```" (mapcar #'account->balance accounts)))
  142. ;; Cron
  143. (defvar *last-entries* (make-hash-table) "Last per-chat entries")
  144. (defvar *tokens* (make-hash-table) "Per-chat access tokens")
  145. (defmacro with-chat-credentials ((chat-id) &body body)
  146. `(let* ((*access-token* (gethash ,chat-id *tokens*))
  147. (*credentials-provider* (lambda (authenticator)
  148. (with-secret (login-pass (list :raiffeisen ,chat-id))
  149. (if login-pass
  150. (apply authenticator login-pass)
  151. (error "no raiffeisen credentials for ~A" ,chat-id))))))
  152. (prog1 (progn ,@body)
  153. (setf (gethash ,chat-id *tokens*) *access-token*))))
  154. (defun get-chat-last-n-entries (chat-id &optional (count 10))
  155. (with-chat-credentials (chat-id)
  156. (let ((accounts (^account))
  157. (transactions (remove nil (agets (^transaction :size count) "list")
  158. :key (agetter "type"))))
  159. (sort (loop for tr in transactions
  160. collect (move->entry (append tr (list (cons "account" (get-transaction-account tr accounts))))))
  161. #'< :key #'pta-ledger:entry-date))))
  162. (defun get-chat-accounts (chat-id)
  163. (with-chat-credentials (chat-id)
  164. (concatenate 'list (^account) (^loan))))
  165. (defcron process-raiffeisen (:minute '(member 0 5 10 15 20 25 30 35 40 45 50 55))
  166. (dolist (chat-id (lists-get :raiffeisen))
  167. (let* ((old (gethash chat-id *last-entries*))
  168. (new (get-chat-last-n-entries chat-id 20))
  169. (changes (sort (set-difference new old :test #'equalp)
  170. #'< :key #'pta-ledger:entry-date))
  171. (ledger-package (find-package :chatikbot.plugins.ledger)))
  172. (when changes
  173. (log:info changes)
  174. (when old
  175. (if ledger-package
  176. (let ((new-chat-entry (symbol-function
  177. (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
  178. (dolist (entry changes)
  179. (funcall new-chat-entry chat-id (pta-ledger:clone-entry entry))))
  180. (bot-send-message chat-id (format-entries changes) :parse-mode "markdown")))
  181. (let ((merged (merge 'list old changes #'< :key #'pta-ledger:entry-date)))
  182. (setf (gethash chat-id *last-entries*)
  183. (subseq merged (max (- (length merged) 200) 0))))))))
  184. (def-message-cmd-handler handler-raif (:raif)
  185. (let ((arg (car args)))
  186. (if (string= arg "bal")
  187. (bot-send-message chat-id (format-balance (get-chat-accounts chat-id)) :parse-mode "markdown")
  188. (let ((last (get-chat-last-n-entries chat-id (if arg (parse-integer arg) 10))))
  189. (bot-send-message chat-id (format-entries last) :parse-mode "markdown")))))