;; ;; 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 ) (import scheme (chicken base) (chicken string) (chicken io) (chicken irregex) (chicken sort) (chicken process-context) (chicken pathname) (chicken condition) bank-account member-record members-base bank-fio dictionary member-fees period configuration utils progress) ;; Exchange rates (define exchange-rates-lookup-table (make-period-lookup-table '(((2010 1) 25)))) ;; Lookup CZK/EUR (define (lookup-eur-rate) (car (lookup-by-period exchange-rates-lookup-table))) ;; Extract probable member-id from transaction (define (transaction-extract-member-id transaction) (if (equal? (bank-transaction-type transaction) "Poplatek") #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) (let loop ((mb mb) (transactions (bank-account-transactions ba))) (if (null? transactions) mb (let* ((transaction (car transactions)) (varsym-id (transaction-extract-member-id transaction)) (member (find-member-by-id mb varsym-id))) (loop (if member (members-base-update mb (lambda (mr) (compare-member-id (member-id mr) varsym-id)) (lambda (mr) (member-record-add-payment mr transaction))) (if (and (or (not last-checked) (> (bank-transaction-id transaction) last-checked)) (> (bank-transaction-amount transaction) 0)) (members-base-add-unpaired mb transaction) mb)) (cdr transactions)))))) ;; Reads the payments (define (load-accounts-list apikeys) (call/cc (lambda (ret) (with-exception-handler (lambda (ex) (ret #f)) (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)) (accounts (if acc-list (load-accounts acc-list dir) #f)) (total (length accounts)) (last-checked (get-latest-checked-id checked-fn))) (if accounts (with-progress% #t "Payments" (progress%-advance 0) (let loop ((mb mb) (idx 0) (accounts accounts)) (if (null? accounts) (let ((mb (members-base-update mb identity member-sort-payments))) (progress%-advance 1) mb) (let () (progress%-advance (/ idx total)) (loop (members-payments-process-bank mb (car accounts) last-checked) (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) (dict-set mr 'payments (sort (dict-ref mr 'payments '()) (lambda (a b) (string