1
0

raiffeisen.lisp 9.0 KB

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