;; ;; members-payments.scm ;; ;; Adding payment information to member records from bank account statement. ;; ;; ISC License ;; ;; Copyright 2023 Brmlab, z.s. ;; Dominik Pantůček ;; ;; Permission to use, copy, modify, and/or distribute this software ;; for any purpose with or without fee is hereby granted, provided ;; that the above copyright notice and this permission notice appear ;; in all copies. ;; ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL ;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED ;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE ;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR ;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ;; (declare (unit members-payments)) (module members-payments ( member-payments-total members-payments-process member-balance member-total-balance member-to-notify? members-to-notify brmember-balance-history ) (import scheme (chicken base) (chicken string) (chicken io) (chicken irregex) (chicken sort) (chicken process-context) (chicken pathname) (chicken condition) util-list bank-account brmember mbase bank-fio util-dict-list members-fees cal-period configuration progress specification cal-day) ;; Transaction types to ignore (define ignored-transaction-types '("Poplatek" "Připsaný úrok")) ;; Lookup CZK/EUR (define (lookup-eur-rate) (car (lookup-by-cal-period exchange-rates-lookup-table))) ;; Extract probable member-id from transaction (define (transaction-extract-member-id transaction) (if (member (bank-transaction-type transaction) ignored-transaction-types) #f (let* ((varsym-id0 (string->number (bank-transaction-varsym transaction))) (varsym-id (or varsym-id0 (let* ((msg (bank-transaction-message transaction)) (ci (substring-index "," msg)) (vs (if ci (substring msg 0 ci) msg))) (string->number vs))))) varsym-id))) ;; Special comparator (originally with JendaSAP hack) (define (compare-member-id member-id bank-varsym) (eq? member-id bank-varsym)) ;; Merges bank account statement into members payment keys. The ;; payment key will be a list of transactions. (define (members-payments-process-bank mb ba last-checked all-accounts) (let loop ((mb mb) (transactions (bank-account-transactions ba))) (if (null? transactions) mb (let* ((transaction (car transactions)) (varsym-id (transaction-extract-member-id transaction)) (bmember (find-member-by-id mb varsym-id))) (loop (if bmember (mbase-update-by-id mb varsym-id (lambda (mr) (brmember-add-payment mr transaction))) (if (and (or (not last-checked) (> (bank-transaction-id transaction) last-checked)) (> (bank-transaction-amount transaction) 0) (not (bank-accounts-member? all-accounts (bank-transaction-account transaction) (bank-transaction-bank transaction))) (not (member (bank-transaction-type transaction) ignored-transaction-types))) (mbase-add-unpaired mb transaction) mb)) (cdr transactions)))))) ;; Reads the accounts list, returns '() upon error. (define (load-accounts-list apikeys) (call/cc (lambda (ret) (with-exception-handler (lambda (ex) (ret '())) (lambda () (map (compose car string-split) (read-lines (open-input-file apikeys)))))))) ;; Loads all accounts - it expects .csv files in given directory. (define (load-accounts accounts-list dir) (map (lambda (acc) (bank-fio-parse (make-pathname dir (string-append acc ".csv")))) accounts-list)) ;; Reads single number from given file and returns it. If no number ;; is and/or the file does not exist, returns #f. (define (get-latest-checked-id file-name) (call/cc (lambda (ret) (with-exception-handler (lambda (ex) (ret #f)) (lambda () (with-input-from-file file-name (lambda () (let ((str (read-line))) (string->number str))))))))) ;; If apikeys is not #f, loads the account numbers, loads bank ;; accounts and processes transactions. (define (members-payments-process mb apikeys-file dir checked-fn) (if apikeys-file (let* ((acc-list (load-accounts-list apikeys-file)) (all-accounts (if acc-list (load-accounts acc-list dir) #f)) (total (length all-accounts)) (last-checked (get-latest-checked-id checked-fn))) (if all-accounts (with-progress% #t "Payments" (progress%-advance 0) (let loop ((mb mb) (idx 0) (accounts all-accounts)) (if (null? accounts) (let ((mb (mbase-update mb identity member-sort-payments))) (progress%-advance 1) mb) (let () (progress%-advance (/ idx total)) (loop (if (car accounts) (members-payments-process-bank mb (car accounts) last-checked all-accounts) mb) (add1 idx) (cdr accounts)))))) (let () (print "Warning: no accounts loaded!") mb))) mb)) ;; Adds all balances - payments are converted to CZK in member-payments-total (define (member-sort-payments mr) (ldict-set mr 'payments (sort (ldict-ref mr 'payments '()) (lambda (a b) (stringbalance-history mc) (filter (lambda (mcr) (not (= (cadr mcr) 0))) (map (lambda (mce) (let ((fee (member-calendar-entry->fee mce))) (list (cal-ensure-day (car mce)) (- fee) 'CZK (cadr mce) (- fee) "Fee"))) mc))) ;; Converts bank transactions to transactions usable in balance ;; history (define (transactions->balance-history bts) (map (lambda (bt) (let ((curr (bank-transaction-currency bt)) (amt (bank-transaction-amount bt)) (day (parse-cal-day/month (bank-transaction-date bt)))) (list day amt curr (bank-transaction-message bt) (if (eq? curr 'CZK) amt (parameterize ((*current-month* (cal-ensure-month day))) (* amt (lookup-eur-rate)))) "Payment"))) bts)) ;; Converts credit records to transactions usable in balance history (define (credits->balance-history crs) (map (lambda (cr) (list (cal-ensure-day (cadr cr)) (car cr) "CZK" (caddr cr) (car cr) "Credit")) crs)) ;; Returns a single credit/debit list of payments and fees ;; calendar. The result is a list of lists: ;; (list balance day amount currency message/comment czk-amount type-string) (define (brmember-balance-history mr) (let* ((mcal (calendar->balance-history (member-calendar mr))) (pmts (transactions->balance-history (brmember-payments mr))) (crs (credits->balance-history (brmember-credit mr))) (all-unsorted (append mcal pmts crs)) (all (sort all-unsorted (lambda (a b) (cal-day