ofd.lisp 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. (in-package :cl-user)
  2. (defpackage chatikbot.plugins.ofd
  3. (:use :cl :chatikbot.common :alexandria))
  4. (in-package :chatikbot.plugins.ofd)
  5. (defparameter +api-root+ "https://proverkacheka.nalog.ru:9999/v1/")
  6. (defmethod poller-request ((module (eql :ofd)) method &rest params)
  7. (handler-case
  8. (json-request (concatenate 'string +api-root+ method)
  9. :basic-auth *poller-token*
  10. :headers '((:device-id . "bot") (:device-os . "lisp"))
  11. :parameters (rest-parameters params t))
  12. (dex:http-request-failed (e) e)))
  13. (defmethod poller-validate ((module (eql :ofd)) response)
  14. (not (typep response 'dex:http-request-failed)))
  15. (defmethod poller-get-token ((module (eql :ofd)) secret)
  16. (let* ((*poller-token* secret))
  17. (ignore-errors
  18. (poller-request :ofd "mobile/users/login")
  19. *poller-token*)))
  20. (defun login ()
  21. (poller-request :ofd "mobile/users/login"))
  22. (defun cheque-check (fn fd fp n date sum)
  23. (handler-case
  24. (let ((res (poller-call :ofd
  25. (format nil "ofds/*/inns/*/fss/~A/operations/~A/tickets/~A" fn n fd)
  26. :|fiscalSign| fp
  27. :|date| date
  28. :|sum| sum)))
  29. (typecase res
  30. (dex:http-request-not-acceptable nil)
  31. (t (error res))))
  32. (end-of-file () t)))
  33. (defun cheque-get (fn fd fp)
  34. (handler-case
  35. (let ((res (poller-call :ofd
  36. (format nil "inns/*/kkts/*/fss/~A/tickets/~A" fn fd)
  37. :|fiscalSign| fp
  38. :|sendToEmail| "no")))
  39. (typecase res
  40. (list (agets res "document" "receipt"))
  41. (dex:http-request-not-acceptable nil)
  42. (t res)))
  43. (end-of-file () nil)))
  44. (defun receive (fn fd fp n date sum)
  45. (ignore-errors (login))
  46. (when (handler-case (cheque-check fn fd fp n date sum)
  47. (dex:http-request-internal-server-error ()
  48. (ignore-errors (cheque-check fn fd fp n date sum)))
  49. (error (e) (log:warn e) nil))
  50. (or (cheque-get fn fd fp)
  51. (cheque-get fn fd fp)
  52. (cheque-get fn fd fp))))
  53. (defparameter +qr-re+ (cl-ppcre:create-scanner "t=(\\d{8}T\\d{4,6})&s=(\\d+(?:\\.\\d{2})?)&fn=(\\d{16})&i=(\\d{0,10})&fp=(\\d{0,10})&n=(1|2)"))
  54. (defun parse-qt (str)
  55. (multiple-value-bind (matched groups)
  56. (cl-ppcre:scan-to-strings +qr-re+ str)
  57. (when matched
  58. (destructuring-bind (date sum fn fd fp n) (coerce groups 'list)
  59. (list (parse-integer fn) (parse-integer fd) (parse-integer fp)
  60. (parse-integer n) date (round (* 100 (parse-float sum))))))))
  61. (defvar *default-currency* "RUB")
  62. (defvar *default-expense-account* "expenses")
  63. (defvar *default-asset-account* "assets:Cash:RUB")
  64. (defun expense-account (name &optional user address)
  65. (declare (ignorable name user address))
  66. *default-expense-account*)
  67. (defun expense-comment (name &optional user address price quantity sum)
  68. (declare (ignorable name user address price quantity sum))
  69. (format nil "item: ~A~@[, ~A @ ~$~]"
  70. name
  71. (unless (= quantity 1) quantity)
  72. (unless (= quantity 1) (/ price 100))))
  73. (defun expense-posting (item &optional user address)
  74. (let ((name (agets item "name"))
  75. (price (agets item "price"))
  76. (quantity (agets item "quantity"))
  77. (sum (agets item "sum")))
  78. (pta-ledger:make-posting
  79. :account (expense-account name user address)
  80. :comment (expense-comment name user address price quantity sum)
  81. :amount (pta-ledger:make-amount
  82. :quantity (/ sum 100)
  83. :commodity *default-currency*))))
  84. (defun asset-posting (amount &optional user address)
  85. (declare (ignorable user address))
  86. (pta-ledger:make-posting
  87. :account *default-asset-account*
  88. :amount (pta-ledger:make-amount
  89. :quantity (* -1 amount)
  90. :commodity *default-currency*)))
  91. (defun not-empty (str)
  92. (unless (string= str "")
  93. str))
  94. (defun cheque->entry (c)
  95. (let* ((total-sum (agets c "totalSum"))
  96. (date (local-time:timestamp-to-universal
  97. (local-time:parse-timestring (agets c "dateTime"))))
  98. (inn (not-empty (agets c "userInn")))
  99. (user (or (not-empty (agets c "retailPlace"))
  100. (not-empty (agets c "user"))
  101. (format nil "ИНН ~A" inn)))
  102. (address (not-empty (agets c "retailPlaceAddress")))
  103. (items (agets c "items")))
  104. (pta-ledger:make-entry
  105. :date date
  106. :description user
  107. :comment (format nil "~@[inn: ~A~]~@[, address: ~A~]" inn address)
  108. :postings (append (mapcar #'expense-posting items)
  109. (list (asset-posting (/ total-sum 100) user address))))))
  110. (defun handle-auth (login pass)
  111. (handler-case
  112. (progn
  113. (poller-authenticate :ofd (cons login pass)))
  114. (poller-cant-authenticate ()
  115. (bot-send-message "Чот не смог, пропробуй другие."))))
  116. (def-message-cmd-handler handler-ofd (:ofd)
  117. (cond
  118. ((= 2 (length *args*)) (apply 'handle-auth *args*))
  119. (:otherwise (bot-send-message "/ofd <login> <pass>"))))
  120. (defvar *chat-next-cheque-handlers* (make-hash-table) "chat cheque handlers")
  121. (defun handle-next-cheque (handler)
  122. (setf (gethash *chat-id* *chat-next-cheque-handlers*) handler))
  123. (def-message-handler ofd-handler (-10)
  124. (let ((parsed (parse-qt *text*)))
  125. (when parsed
  126. (telegram-send-chat-action *chat-id* "typing")
  127. (let ((cheque (apply #'receive parsed))
  128. (handler (gethash *chat-id* *chat-next-cheque-handlers*)))
  129. (if cheque
  130. (let ((ledger-package (find-package :chatikbot.plugins.ledger))
  131. (entry (cheque->entry cheque)))
  132. (cond
  133. (handler
  134. (remhash *chat-id* *chat-next-cheque-handlers*)
  135. (funcall handler entry))
  136. (ledger-package
  137. (let ((new-chat-entry (symbol-function
  138. (intern "LEDGER/NEW-CHAT-ENTRY" ledger-package))))
  139. (funcall new-chat-entry *chat-id* (pta-ledger:clone-entry entry))))
  140. (:otherwise (bot-send-message (pta-ledger:render entry) :parse-mode "markdown"))))
  141. (bot-send-message "Не смог в чек :(")))
  142. t)))