Browse Source

Add support for splitting transactions

The format of elbank-data has changed, and is incompatible with previous
versions.
master
Nicolas Petton 2 years ago
parent
commit
c4e645207a
No known key found for this signature in database GPG Key ID: E8BCD7866AFCF978
8 changed files with 390 additions and 228 deletions
  1. +51
    -33
      elbank-boobank.el
  2. +2
    -1
      elbank-budget.el
  3. +137
    -84
      elbank-common.el
  4. +3
    -1
      elbank-overview.el
  5. +55
    -9
      elbank-report.el
  6. +42
    -38
      features/support/env.el
  7. +87
    -61
      test/elbank-boobank-test.el
  8. +13
    -1
      test/elbank-common-test.el

+ 51
- 33
elbank-boobank.el View File

@@ -27,6 +27,7 @@
(require 'map)
(require 'json)
(require 'cl-lib)
(require 'subr-x)

(require 'elbank-common)

@@ -51,17 +52,26 @@
(defun elbank-boobank--scrap-data ()
"Return all data scraped from boobank."
(let* ((accounts (elbank--fetch-boobank-accounts))
(transactions (seq-map (lambda (account)
(list (intern (map-elt account 'id))
(elbank--fetch-boobank-transactions account)))
accounts)))
(message "Elbank: fetching done!")
(transactions (apply #'seq-concatenate 'list
(seq-map #'elbank-boobank--scrap-transactions
accounts))))
`((accounts . ,accounts)
(transactions . ,(map-apply (lambda (key val)
;; Fetched transactions data is a nested
;; vector, so only keep the first one.
(cons key (seq-elt val 0)))
transactions)))))
(transactions . ,transactions))))

(defun elbank-boobank--scrap-transactions (account)
"Return a list of transactions for ACCOUNT scraped from boobank."
(seq-map (lambda (data)
(elbank-boobank--make-transaction data account))
(elbank--fetch-boobank-transactions account)))

(defun elbank-boobank--make-transaction (data account)
"Return a transaction alist from DATA with its account value set to ACCOUNT.

If an account already exists with the same id as ACCOUNT, use
that account instead of the new ACCOUNT."
(let* ((account-to-use (or (elbank-account-with-id (map-elt account 'id))
account)))
(cons (cons 'account account-to-use) data)))

(defun elbank--fetch-boobank-accounts ()
"Return all accounts in boobank."
@@ -89,25 +99,36 @@ The account list is taken from NEW, so accounts not present in
NEW are deleted. New transactions for existing accounts are
*only* added, no transaction is removed."
(if old
`((accounts . ,(map-elt new 'accounts))
`((accounts . ,(elbank--merge-accounts
(map-elt old 'accounts)
(map-elt new 'accounts)))
(transactions . ,(elbank--merge-transactions
(map-elt old 'transactions)
(map-elt new 'transactions))))
new))

(defun elbank--merge-accounts (old new)
"Merge the account list from OLD and NEW.
Data from existing accounts in OLD are updated with new data from
NEW."
(seq-do (lambda (new-acc)
(when-let ((acc (seq-find (lambda (acc)
(equal (map-elt new-acc 'id)
(map-elt acc 'id)))
old)))
(map-apply (lambda (key val)
(map-put acc key val))
new-acc)))
new)
(let ((new-accounts (seq-remove (lambda (acc)
(seq-contains old acc))
new)))
(seq-concatenate 'list old new-accounts)))

(defun elbank--merge-transactions (old new)
"Merge the transaction list from OLD and NEW."
(map-apply (lambda (id transactions)
`(,id . ,(elbank--merge-account-transactions
transactions
(map-elt new id))))
old))

(defun elbank--merge-account-transactions (old new)
"Merge the transactions from OLD and NEW.
OLD and NEW are lists of transactions for the same account."
(let ((new-transactions (elbank--new-transactions old new)))
(seq-concatenate 'vector old new-transactions)))
(seq-concatenate 'list old new-transactions)))

(defun elbank--new-transactions (old new)
"Return all transactions not present in OLD bu present in NEW.
@@ -125,18 +146,15 @@ When comparing transactions, ignore (manually set) categories."

(defun elbank--count-transactions-like (transaction transactions)
"Return the number of transactions like TRANSACTION in TRANSACTIONS."
(seq-count (apply-partially #'elbank--transaction-equal-p transaction)
transactions))

(defun elbank--transaction-equal-p (transaction1 transaction2)
"Return non-nil if TRANSACTION1 equals TRANSACTION2.
Categories are ignored when comparing."
(cl-labels ((without-category (transaction)
(map-remove (lambda (key _)
(eq key 'category))
transaction)))
(equal (without-category transaction1)
(without-category transaction2))))
(seq-length (elbank-filter-transactions
transactions
:raw (map-elt transaction 'raw)
:account (map-elt transaction 'account)
:amount (map-elt transaction 'amount)
:date (map-elt transaction 'date)
:vdate (map-elt transaction 'vdate)
:rdate (map-elt transaction 'rdate)
:label (map-elt transaction 'label))))

(defun elbank--find-boobank-executable ()
"Return the boobank executable.


+ 2
- 1
elbank-budget.el View File

@@ -175,7 +175,8 @@ Values are lists of spent amounts and budgeted amounts for a category."
(when (elbank-transaction-in-category-p trans cat)
cat))
(seq-map #'car elbank-budget)))
(elbank-filter-transactions :period elbank-report-period))))
(elbank-filter-transactions (elbank-all-transactions)
:period elbank-report-period))))

(provide 'elbank-budget)
;;; elbank-budget.el ends here

+ 137
- 84
elbank-common.el View File

@@ -94,11 +94,21 @@ Data is cached to `elbank-data'."

(defun elbank-write-data (data)
"Write DATA to `elbank-data-file'."
(with-temp-file (expand-file-name elbank-data-file)
(emacs-lisp-mode)
(insert ";; This file is automatically generated by Indium.")
(newline)
(insert (format "(setq %s '%S)" "elbank-data" data))))
(let ((print-circle t)
(print-level nil)
(print-length nil))
(with-temp-file (expand-file-name elbank-data-file)
(emacs-lisp-mode)
(insert ";; This file is automatically generated by Indium.")
(newline)
(insert (format "(setq %s '%S)" "elbank-data" data)))))

(defun elbank-account-with-id (id)
"Return the account with ID, or nil."
(seq-find (lambda (acc)
(string= (map-elt acc 'id)
id))
(map-elt elbank-data 'accounts)))

(cl-defgeneric elbank-transaction-elt (transaction key &optional default)
"Return the value of TRANSACTION at KEY.
@@ -109,24 +119,29 @@ If the result is nil, return DEFAULT."
(cl-defmethod elbank-transaction-elt (transaction (_key (eql category)) &optional default)
"Return the category of TRANSACTION.

Split transactions (that hold an list of (CATEGORY . AMOUNT)
elements as category) have no category.

If the result is nil, return DEFAULT."
(let ((case-fold-search t)
(custom-category (map-elt transaction 'category)))
(if (or (null custom-category)
(string-empty-p custom-category))
(seq-find #'identity
(map-apply
(lambda (key category)
(when (seq-find
(lambda (regexp)
(string-match-p
regexp
(map-elt transaction 'raw)))
category)
key))
elbank-categories)
default)
custom-category)))
(cond ((or (null custom-category)
(and (stringp custom-category)
(string-empty-p custom-category)))
(seq-find #'identity
(map-apply
(lambda (key category)
(when (seq-find
(lambda (regexp)
(string-match-p
regexp
(map-elt transaction 'raw)))
category)
key))
elbank-categories)
default))
((stringp custom-category) custom-category)
(t default))))

(cl-defgeneric (setf elbank-transaction-elt) (store transaction key)
(if (map-contains-key transaction key)
@@ -134,63 +149,86 @@ If the result is nil, return DEFAULT."
;; Always mutate transactions in place.
(nconc transaction (list (cons key store)))))

(cl-defun elbank-filter-transactions (&key account-id period category)
"Filter transactions, all keys are optional.
(defun elbank-transaction-split-p (transaction)
"Return non-nil if TRANSACTION is split.
Split transactions have an alist of (CATEGORY-NAME . AMOUNT) as
category."
(let ((category (map-elt transaction 'category)))
(and (not (null category))
(listp category))))

Return transactions in the account with id ACCOUNT-ID for a PERIOD
that belong to CATEGORY.
(defun elbank-sub-transaction-p (transaction)
"Return non-nil if TRANSACTION is a sub transaction.

ACCOUNT-ID is a symbol, PERIOD is a list of the form `(type
time)', CATEGORY is a category string."
(thread-first (elbank-all-transactions)
(elbank--filter-transactions-account-id account-id)
(elbank--filter-transactions-period period)
(elbank--filter-transactions-category category)))
Sub transactions are built dynamically from
`elbank-all-transactions' from split transactions."
(not (null (elbank-transaction-elt transaction 'split-from))))

(defun elbank--filter-transactions-account-id (transactions account-id)
"Return a subset of TRANSACTIONS that belong to ACCOUNT-ID."
(if account-id
(map-elt (map-elt elbank-data 'transactions) account-id)
transactions))
(defmacro elbank-filter-transactions (collection &rest query)
"Returned all transactions in COLLECTION that match QUERY.

(defun elbank--filter-transactions-category (transactions category)
"Return the subset of TRANSACTIONS that belong to CATEGORY.
QUERY is a plist of the form (:KEY1 VAL1 :KEY2 VAL2...) where
keys are keywords matching transaction keys.

CATEGORY is a string of the form \"cat:subcat:subsubcat\"
representing the path of a category."
(if category
(seq-filter (lambda (transaction)
(elbank-transaction-in-category-p transaction category))
transactions)
transactions))
Special keys in QUERY:

(defun elbank-transaction-in-category-p (transaction category)
"Return non-nil if TRANSACTION belongs to CATEGORY."
(string-prefix-p category (elbank-transaction-elt transaction 'category "") t))
- `:category': Match transactions within the category name, using
`elbank-transaction-in-category-p'.

- `:period': The value must be a list of the form (TYPE TIME),
where TYPE is either year or month.

- `:account-id': Lookup an account with the queried id and match
transactions for that account."
(let* ((sym (make-symbol "transaction"))
(query (elbank--make-queries sym query)))
`(seq-filter (lambda (,sym) ,query) ,collection)))

(defun elbank--make-queries (transaction queries)
"Return a form for filtering TRANSACTION with QUERIES."
(let ((filters (seq-map (lambda (query)
(elbank--make-query transaction
(car query)
(cadr query)))
(seq-partition queries 2))))
`(and ,@filters)))

(cl-defgeneric elbank--make-query (transaction key val)
(unless (keywordp key)
(error "Query KEY must be a keyword"))
`(or (null ,val)
(equal (elbank-transaction-elt ,transaction
',(intern (seq-drop (symbol-name key) 1)))
,val)))

(defun elbank--filter-transactions-period (transactions period)
"Return the subset of TRANSACTIONS that are within PERIOD.
(cl-defmethod elbank--make-query (transaction (_key (eql :category)) val)
`(elbank-transaction-in-category-p ,transaction ,val))

(cl-defmethod elbank--make-query (transaction (_key (eql :account-id)) val)
(elbank--make-query transaction :account `(elbank-account-with-id ,val)))

(cl-defmethod elbank--make-query (transaction (_key (eql :period)) period)
"Return a period query for TRANSACTION.

PERIOD is a list of the form `(type time)', with `type' a
symbol (`month' or `year'), and `time' an encoded time."
(pcase (car period)
(`year (elbank--filter-transactions-period-format transactions
(cadr period)
"%Y"))
(`month (elbank--filter-transactions-period-format transactions
(cadr period)
"%Y-%m"))
(`nil transactions)
(_ (error "Invalid period type %S" (car period)))))

(defun elbank--filter-transactions-period-format (transactions time format)
"Return the subset of TRANSACTIONS within TIME.
Comparison is done by formatting periods using FORMAT."
(seq-filter (lambda (transaction)
(let ((tr-time (elbank--transaction-time transaction)))
(string= (format-time-string format time)
(format-time-string format tr-time))))
transactions))
(let ((period-var (make-symbol "period")))
`(let ((,period-var ,period))
(pcase (car ,period-var)
(`year (elbank--make-period-query-format ,transaction
(cadr ,period-var)
"%Y"))
(`month (elbank--make-period-query-format ,transaction
(cadr ,period-var)
"%Y-%m"))
(`nil t)
(_ (error "Invalid period type %S" (car ,period-var)))))))

(defun elbank--make-period-query-format (transaction time format)
"Return a period query for TRANSACTION and TIME.
Comparison is done by formatting times using FORMAT."
(string= (format-time-string format time)
(format-time-string format (elbank--transaction-time transaction))))

(defun elbank--transaction-time (transaction)
"Return the encoded time for TRANSACTION."
@@ -199,6 +237,11 @@ Comparison is done by formatting periods using FORMAT."
(or el 0))
(parse-time-string (elbank-transaction-elt transaction 'date)))))

(defun elbank-transaction-in-category-p (transaction category)
"Return non-nil if TRANSACTION belongs to CATEGORY."
(or (null category)
(string-prefix-p category (elbank-transaction-elt transaction 'category "") t)))

(defun elbank-sum-transactions (transactions)
"Return the sum of all TRANSACTIONS.
TRANSACTIONS are expected to all use the same currency."
@@ -213,9 +256,10 @@ TRANSACTIONS are expected to all use the same currency."
(seq-sort #'time-less-p
(seq-uniq
(seq-map (lambda (transaction)
(encode-time 0 0 0 1 1 (seq-elt (decode-time
(elbank--transaction-time transaction))
5)))
(encode-time 0 0 0 1 1
(seq-elt (decode-time
(elbank--transaction-time transaction))
5)))
(elbank-all-transactions)))))

(defun elbank-transaction-months ()
@@ -228,20 +272,29 @@ TRANSACTIONS are expected to all use the same currency."
(encode-time 0 0 0 1 (seq-elt time 4) (seq-elt time 5))))
(elbank-all-transactions)))))

(defun elbank-all-transactions ()
"Return all transactions for all accounts."
(seq-remove #'seq-empty-p
(apply #'seq-concatenate
'vector
(map-values (map-elt elbank-data 'transactions)))))

(defun elbank-account (id)
"Return the account with ID, or nil."
(unless (stringp id)
(setq id (symbol-name id)))
(seq-find (lambda (account)
(string= id (map-elt account 'id)))
(map-elt elbank-data 'accounts)))
(defun elbank-all-transactions (&optional nosplit)
"Return all transactions for all accounts.

Unless NOSPLIT is non-nil, split transactions that have multiple
categories into multiple transactions."
(if nosplit
(map-elt elbank-data 'transactions)
(seq-mapcat (lambda (transaction)
(if (elbank-transaction-split-p transaction)
(seq-map (lambda (cat)
(let ((sub-trans (copy-alist transaction)))
(map-put sub-trans 'amount (cdr cat))
(map-put sub-trans 'category (car cat))
(map-put sub-trans 'label
(format "[split] %s"
(elbank-transaction-elt
transaction
'label)))
(map-put sub-trans 'split-from transaction)
sub-trans))
(map-elt transaction 'category))
(list transaction)))
(elbank-all-transactions t))))

(defun elbank--longest-account-label ()
"Return the longest account label from all accoutns."


+ 3
- 1
elbank-overview.el View File

@@ -141,7 +141,9 @@ If nothing important is at point, return nil."
(defun elbank-overview-update-data ()
"Read new data from boobank and update the buffer."
(interactive)
(message "Elbank: updating...")
(elbank-boobank-update)
(message "Elbank: done!")
(elbank-overview-update-buffer))

(defun elbank-overview--insert-hr ()
@@ -241,7 +243,7 @@ If nothing important is at point, return nil."

(defun elbank-overview--list-transactions (account)
"Display the list of transactions for ACCOUNT."
(elbank-report :account-id (intern (map-elt account 'id))
(elbank-report :account-id (map-elt account 'id)
:reverse-sort t))

(provide 'elbank-overview)


+ 55
- 9
elbank-report.el View File

@@ -206,7 +206,7 @@ Return the report buffer."
(label (completing-read "Select account: " labels)))
(setq elbank-report-account-id
(when-let ((position (seq-position labels label)))
(intern (map-elt (seq-elt accounts position) 'id)))))
(map-elt (seq-elt accounts position) 'id))))
(elbank-report-refresh))

(defun elbank-report-filter-period ()
@@ -278,9 +278,10 @@ When `elbank-report-inhibit-update' is non-nil, do not update."
(unless elbank-report-inhibit-update
(let ((inhibit-read-only t)
(transactions (elbank-filter-transactions
(elbank-all-transactions)
:account-id elbank-report-account-id
:category elbank-report-category
:period elbank-report-period))
:period elbank-report-period
:category elbank-report-category))
(inhibit-read-only t))
(let ((pos (point)))
(erase-buffer)
@@ -296,18 +297,62 @@ When `elbank-report-inhibit-update' is non-nil, do not update."
(elbank-report--insert-sum transactions))
(goto-char (min (point-max) pos))))))

(defun elbank-report-set-category (category)
"Prompt for a CATEGORY for the transaction at point."
(defun elbank-report-set-category (category &optional transaction)
"Update the CATEGORY of TRANSACTION.
When called interactively, prompt for the category.

If TRANSACTION is nil, set the category of the transaction at
point."
(interactive (list (completing-read "Category: "
(map-keys elbank-categories))))
(setf (elbank-transaction-elt (elbank-report--transaction-at-point) 'category)
(unless transaction
(setq transaction (elbank-report--transaction-at-point)))
(setf (elbank-transaction-elt transaction 'category)
category)
(elbank-write-data elbank-data)
(elbank-report-refresh))

(defun elbank-report-split-transaction ()
"Split the transaction at point.

Splitting is done by assigning multiple categories to
transaction, each one with an amount."
(interactive)
(let* ((trans (elbank-report--transaction-at-point))
(amount-left (string-to-number
(elbank-transaction-elt trans 'amount)))
(categories '()))
(when (elbank-sub-transaction-p trans)
(user-error "Cannot split sub transactions"))
(while (not (zerop amount-left))
(let ((label (completing-read "Category: "
(map-keys elbank-categories)))
(amount (read-from-minibuffer
(format "Amount (%s left): " amount-left)
(number-to-string amount-left))))
(push (cons label amount) categories)
(setq amount-left (/ (round (* 100 (- amount-left
(string-to-number amount))))
100.0 ))))
(elbank-report-set-category categories)))

(defun elbank-report-unsplit-transaction ()
"Unsplit the parent of the sub transaction at point.

Combining the parent is done by setting its category to nil."
(interactive)
(let ((trans (elbank-report--transaction-at-point)))
(unless (elbank-sub-transaction-p trans)
(user-error "Cannot combine transaction"))
(elbank-report-set-category nil (elbank-transaction-elt trans 'split-from))))

(defun elbank-report--transaction-at-point ()
"Return the transaction at point."
(get-text-property (point) 'transaction))
"Return the transaction at point.

Signal an error if there is no transaction at point."
(let ((tr (get-text-property (point) 'transaction)))
(unless tr (user-error "No transaction at point"))
tr))

(defun elbank-report--insert-preambule ()
"Display the report filters in the current buffer."
@@ -324,7 +369,8 @@ When `elbank-report-inhibit-update' is non-nil, do not update."
(insert (format "%s" (cdr filter)))
(insert "\n")))
`(("Account:" . ,(and elbank-report-account-id
(map-elt (elbank-account elbank-report-account-id)
(map-elt (elbank-account-with-id
elbank-report-account-id)
'label)))
("Period:" . ,(and elbank-report-period
(elbank-format-period elbank-report-period)))


+ 42
- 38
features/support/env.el View File

@@ -22,55 +22,59 @@
;; Before anything has run
(setq real-elbank-data elbank-data)
(setq elbank-data
'((accounts . [((id . "1234@fakebank")
'((accounts . [#1=((id . "1234@fakebank")
(label . "Fake account 1")
(currency . "EUR")
(iban . "1234")
(type . 1)
(balance . "400"))

((id . "1235@fakebank")
#2=((id . "1235@fakebank")
(label . "Fake account 2")
(currency . "EUR")
(iban . "1235")
(type . 1)
(balance . "50"))])
(transactions
(1234@fakebank . [((id . "@fakebank")
(date . "2017-11-24")
(rdate . "2017-11-24")
(type . 1)
(raw . "CB Supermarket 1")
(label . "CB Supermarket 1")
(amount . "-124.00"))
((id . "@fakebank")
(date . "2017-11-20")
(rdate . "2017-11-20")
(type . 1)
(label . "Paycheck")
(raw . "Transfer company XX paycheck")
(amount . "2300.00"))
((id . "@fakebank")
(date . "2017-11-20")
(rdate . "2017-11-20")
(type . 1)
(raw . "Bakery xxx")
(label . "Bakery xxx")
(amount . "-4.25"))
((id . "@fakebank")
(date . "2017-11-18")
(rdate . "2017-11-18")
(type . 1)
(raw . "CB Restaurant")
(label . "CB Restaurant")
(amount . "-31.00"))
((id . "@fakebank")
(date . "2017-11-01")
(rdate . "2017-11-01")
(type . 1)
(label . "Rent")
(raw . "Rent November 2017")
(amount . "-450.00"))]))))
(transactions . (((id . "@fakebank")
(date . "2017-11-24")
(rdate . "2017-11-24")
(type . 1)
(raw . "CB Supermarket 1")
(label . "CB Supermarket 1")
(amount . "-124.00")
(account . #1#))
((id . "@fakebank")
(date . "2017-11-20")
(rdate . "2017-11-20")
(type . 1)
(label . "Paycheck")
(raw . "Transfer company XX paycheck")
(amount . "2300.00")
(account . #1#))
((id . "@fakebank")
(date . "2017-11-20")
(rdate . "2017-11-20")
(type . 1)
(raw . "Bakery xxx")
(label . "Bakery xxx")
(amount . "-4.25")
(account . #1#))
((id . "@fakebank")
(date . "2017-11-18")
(rdate . "2017-11-18")
(type . 1)
(raw . "CB Restaurant")
(label . "CB Restaurant")
(amount . "-31.00")
(account . #1#))
((id . "@fakebank")
(date . "2017-11-01")
(rdate . "2017-11-01")
(type . 1)
(label . "Rent")
(raw . "Rent November 2017")
(amount . "-450.00")
(account . #1#))))))

(setq real-elbank-categories elbank-categories)
(setq elbank-categories '(("Expenses:Food" . ("supermarket"


+ 87
- 61
test/elbank-boobank-test.el View File

@@ -27,76 +27,102 @@

(require 'buttercup)
(require 'map)
(require 'seq)
(require 'cl-lib)
(require 'elbank-boobank)

(describe "Merging data"
(it "should keep new accounts"
(let* ((old '((accounts . [((id . "account1"))
((id . "account2"))])))
(new '((accounts . [((id . "account1"))
((id . "account3"))])))
(merged (elbank--merge-data old new)))
(expect (map-elt merged 'accounts) :to-equal
[((id . "account1"))
((id . "account3"))])))

(it "should append new transactions and keep old ones"
(let* ((old `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "1"))
((label "2"))
((label "3"))]))))
(new `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "4"))
((label "5"))]))))
(let* ((old `((accounts . (#1=((id . "account1") (label . "account 1"))))
(transactions . (((label . "1") (account . #1#))
((label . "2") (account . #1#))
((label . "3") (account . #1#))))))
(new `((accounts . (#1=((id . "account1") (label . "account 1"))))
(transactions . (((label . "4") (account . #1#))
((label . "5") (account . #1#))))))
(merged (elbank--merge-data old new)))
(expect (map-elt merged 'transactions) :to-equal
'((account1 . [((label "1"))
((label "2"))
((label "3"))
((label "4"))
((label "5"))])))))
(expect (seq-map (lambda (tr)
(elbank-transaction-elt tr 'label))
(map-elt merged 'transactions))
:to-equal '("1" "2" "3" "4" "5"))))

(it "should keep existing accounts when merging accounts"
(let* ((old '(((id . 1))
((id . 2))))
(new '(((id . 1))
((id . 3))))
(merged (elbank--merge-accounts old new)))
(expect (seq-length merged) :to-be 3)
(expect (car merged) :to-be (car old))
(expect (cadr merged) :to-be (cadr old))
(expect (cl-caddr merged) :to-be (cadr new))))

(it "merging accounts should update current accounts values"
(let* ((old '(((id . 1) (balance . "3000"))))
(new '(((id . 1) (balance . "3500"))))
(merged (elbank--merge-accounts old new)))
(expect (seq-length merged) :to-be 1)
(expect (map-elt (car merged) 'balance) :to-equal "3500")))

(it "should deduplicate new transactions"
(let* ((old `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "1"))
((label "2"))
((label "3"))
((label "3"))]))))
(new `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "2"))
((label "3"))
((label "3"))
((label "3"))
((label "3"))]))))
(let* ((old `((transactions . (((label . "1"))
((label . "2"))
((label . "3"))
((label . "3"))))))
(new `((transactions . (((label . "2"))
((label . "3"))
((label . "3"))
((label . "3"))
((label . "3"))))))
(merged (elbank--merge-data old new)))
(expect (map-elt merged 'transactions) :to-equal
'((account1 . [((label "1"))
((label "2"))
((label "3"))
((label "3"))
((label "3"))
((label "3"))])))))

(it "should ignore categories when deduplicating"
(let* ((old `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "1"))
((label "2"))
((label "3") (category . "foo"))
((label "3") (category . "bar"))]))))
(new `((accounts . [((id . "account1") (label . "account 1"))])
(transactions (account1 . [((label "2"))
((label "3"))
((label "3"))
((label "3"))
((label "3"))]))))
(expect (seq-map (lambda (tr)
(elbank-transaction-elt tr 'label))
(map-elt merged 'transactions))
:to-equal
'("1" "2" "3" "3" "3" "3"))))

(it "should ignore categories when deduplicating"
(let* ((old `((accounts . (((id . "account1") (label . "account 1"))))
(transactions . (((label . "1"))
((label . "2"))
((label . "3") (category . "foo"))
((label . "3") (category . "bar"))))))
(new `((accounts . (((id . "account1") (label . "account 1"))))
(transactions . (((label . "2"))
((label . "3"))
((label . "3"))
((label . "3"))
((label . "3"))))))
(merged (elbank--merge-data old new)))
(expect (map-elt merged 'transactions) :to-equal
'((account1 . [((label "1"))
((label "2"))
((label "3") (category . "foo"))
((label "3") (category . "bar"))
((label "3"))
((label "3"))]))))))
(expect (seq-map (lambda (tr)
(elbank-transaction-elt tr 'label))
(map-elt merged 'transactions))
:to-equal
'("1" "2" "3" "3" "3" "3"))))

(it "should ignore alist elements order deduplicating"
(let* ((old `((transactions . (((label . "1") (amount . "10"))
((label . "2") (amount . "20"))
((amount . "15") (label . "3"))
((label . "3"))))))
(new `((transactions . (((amount . "20") (label . "2"))
((amount . "15") (label . "3"))
((label . "3"))
((label . "3"))))))
(merged (elbank--merge-data old new)))
(expect (seq-map (lambda (tr)
(elbank-transaction-elt tr 'label))
(map-elt merged 'transactions))
:to-equal
'("1" "2" "3" "3" "3"))))

(it "should use existing accounts when making new transactions"
(let* ((account '((id . "account1") (label . "account 1")))
(new-account '((id . "account1") (label . "account 1")))
(elbank-data `((accounts . (,account))))
(transaction (elbank-boobank--make-transaction `((amount . "3000"))
new-account)))
(expect (elbank-transaction-elt transaction 'account) :to-be account))))

(provide 'elbank-boobank-test)
;;; elbank-boobank-test.el ends here

+ 13
- 1
test/elbank-common-test.el View File

@@ -48,7 +48,19 @@
(tr original))
(setf (elbank-transaction-elt tr 'category) "Income")
(expect (elbank-transaction-elt tr 'category) :to-equal "Income")
(expect original :to-be tr))))
(expect original :to-be tr)))

(it "split transactions should have no category"
(let ((tr '((category . (("foo" . 20) ("bar" . 10))))))
(expect (elbank-transaction-elt tr 'category) :to-be nil)
(expect (elbank-transaction-in-category-p tr "foo") :to-be nil)
(expect (elbank-transaction-in-category-p tr "bar") :to-be nil)))

(it "transactions with multiple categories should be split."
(let ((tr1 '((category . (("foo" . 20) ("bar" . 10)))))
(tr2 '((category . "foo"))))
(expect (elbank-transaction-split-p tr1) :to-be-truthy)
(expect (elbank-transaction-split-p tr2) :to-be nil))))

(provide 'elbank-common-test)
;;; elbank-common-test.el ends here

Loading…
Cancel
Save