AtlatestRepositorytally
1
;;; Test suite for (tally sync)2
;;;3
;;; Tests the mapping and deduplication logic using fixture data.4
;;; Does not require a Wise API connection.6
(import (sigil test)7
(sigil string)8
(sigil struct)9
(wise)10
(ledger)11
(tally sync))13
;; ========== Transaction Mapping ==========15
(test-group "tally-map-transaction - basic DEBIT"16
(test "maps a debit transaction to journal entry"17
(let* ((txn (wise-transaction18
type: "DEBIT"19
date: "2026-03-15T14:30:00Z"20
amount: -42.9921
currency: "EUR"22
details: #{ description: "Amazon purchase" }23
reference-number: "TRF-12345"24
running-balance: 1234.56))25
(result (tally-map-transaction txn)))26
(assert-equal "2026-03-15" (journal-transaction-date result))27
(assert-equal "*" (journal-transaction-status result))28
(assert-equal "TRF-12345" (journal-transaction-code result))29
(assert-equal "Amazon purchase" (journal-transaction-description result))30
;; Check ref tag for deduplication31
(assert-equal "TRF-12345" (cdr (assoc "ref" (journal-transaction-tags result))))32
;; Check postings33
(let ((postings (journal-transaction-postings result)))34
(assert-equal 2 (length postings))35
;; First posting: wise balance36
(let ((wise-posting (car postings)))37
(assert-equal "assets:wise:eur" (journal-posting-account wise-posting))38
(assert-equal -42.99 (journal-amount-quantity (journal-posting-amount wise-posting)))39
(assert-equal "EUR" (journal-amount-commodity (journal-posting-amount wise-posting)))40
;; Balance assertion41
(assert-true (journal-amount? (journal-posting-balance-assertion wise-posting)))42
(assert-equal 1234.56 (journal-amount-quantity (journal-posting-balance-assertion wise-posting))))43
;; Second posting: expense (inferred amount)44
(let ((expense-posting (cadr postings)))45
(assert-equal "expenses:uncategorized" (journal-posting-account expense-posting))46
(assert-false (journal-posting-amount expense-posting)))))))48
(test-group "tally-map-transaction - basic CREDIT"49
(test "maps a credit transaction to journal entry"50
(let* ((txn (wise-transaction51
type: "CREDIT"52
date: "2026-03-20T10:00:00Z"53
amount: 500.0054
currency: "USD"55
details: #{ description: "Salary payment" }56
reference-number: "TRF-67890"57
running-balance: 2500.00))58
(result (tally-map-transaction txn)))59
(assert-equal "2026-03-20" (journal-transaction-date result))60
(assert-equal "Salary payment" (journal-transaction-description result))61
;; Wise posting62
(let ((wise-posting (car (journal-transaction-postings result))))63
(assert-equal "assets:wise:usd" (journal-posting-account wise-posting))64
(assert-equal 500.0 (journal-amount-quantity (journal-posting-amount wise-posting))))65
;; Income posting66
(let ((income-posting (cadr (journal-transaction-postings result))))67
(assert-equal "income:uncategorized" (journal-posting-account income-posting))))))69
(test-group "tally-map-transaction - merchant details"70
(test "extracts merchant name from details"71
(let* ((txn (wise-transaction72
type: "DEBIT"73
date: "2026-03-15T12:00:00Z"74
amount: -15.5075
currency: "GBP"76
details: #{ merchant: #{ name: "Costa Coffee" } }77
reference-number: "TRF-COFFEE"78
running-balance: 100.00))79
(result (tally-map-transaction txn)))80
(assert-equal "Costa Coffee" (journal-transaction-description result)))))82
(test-group "tally-map-transaction - zero running balance"83
(test "includes balance assertion when running balance is 0"84
(let* ((txn (wise-transaction85
type: "DEBIT"86
date: "2026-03-15T12:00:00Z"87
amount: -10.0088
currency: "EUR"89
details: #{ description: "Test" }90
reference-number: "TRF-TEST"91
running-balance: 0))92
(result (tally-map-transaction txn))93
(wise-posting (car (journal-transaction-postings result))))94
(assert-true (journal-amount? (journal-posting-balance-assertion wise-posting)))95
(assert-equal 0 (journal-amount-quantity (journal-posting-balance-assertion wise-posting))))))97
(test-group "tally-map-transaction - no running balance"98
(test "omits balance assertion when running balance is #f"99
(let* ((txn (wise-transaction100
type: "DEBIT"101
date: "2026-03-15T12:00:00Z"102
amount: -10.00103
currency: "EUR"104
details: #{ description: "Test" }105
reference-number: "TRF-TEST"106
running-balance: #f))107
(result (tally-map-transaction txn))108
(wise-posting (car (journal-transaction-postings result))))109
(assert-false (journal-posting-balance-assertion wise-posting)))))111
;; ========== Cost Notation (Currency Conversion) ==========113
(test-group "tally-map-transaction - cost notation with exchange rate"114
(test "adds per-unit cost when details contain rate and target currency"115
(let* ((txn (wise-transaction116
type: "DEBIT"117
date: "2026-03-15T12:00:00Z"118
amount: -500.00119
currency: "EUR"120
details: #{ description: "Currency conversion"121
rate: 1.08122
targetCurrency: "USD" }123
reference-number: "TRF-CONV-001"124
running-balance: 500.00))125
(result (tally-map-transaction txn))126
(wise-posting (car (journal-transaction-postings result)))127
(cost (journal-posting-cost wise-posting)))128
(assert-true (journal-cost? cost))129
(assert-equal 'per-unit (journal-cost-type cost))130
(assert-equal 1.08 (journal-amount-quantity (journal-cost-amount cost)))131
(assert-equal "USD" (journal-amount-commodity (journal-cost-amount cost))))))133
(test-group "tally-map-transaction - cost notation with source currency"134
(test "adds per-unit cost using source currency when target matches posting"135
(let* ((txn (wise-transaction136
type: "CREDIT"137
date: "2026-03-15T12:00:00Z"138
amount: 540.00139
currency: "USD"140
details: #{ description: "Currency conversion"141
rate: 0.9259142
sourceCurrency: "EUR"143
targetCurrency: "USD" }144
reference-number: "TRF-CONV-002"145
running-balance: 1540.00))146
(result (tally-map-transaction txn))147
(wise-posting (car (journal-transaction-postings result)))148
(cost (journal-posting-cost wise-posting)))149
(assert-true (journal-cost? cost))150
(assert-equal 'per-unit (journal-cost-type cost))151
(assert-equal 0.9259 (journal-amount-quantity (journal-cost-amount cost)))152
(assert-equal "EUR" (journal-amount-commodity (journal-cost-amount cost))))))154
(test-group "tally-map-transaction - no cost without exchange info"155
(test "omits cost when no rate in details"156
(let* ((txn (wise-transaction157
type: "DEBIT"158
date: "2026-03-15T12:00:00Z"159
amount: -42.99160
currency: "EUR"161
details: #{ description: "Regular purchase" }162
reference-number: "TRF-REGULAR"163
running-balance: 957.01))164
(result (tally-map-transaction txn))165
(wise-posting (car (journal-transaction-postings result))))166
(assert-false (journal-posting-cost wise-posting)))))168
(test-group "tally-map-transaction - no cost when same currency"169
(test "omits cost when target currency matches posting currency"170
(let* ((txn (wise-transaction171
type: "DEBIT"172
date: "2026-03-15T12:00:00Z"173
amount: -100.00174
currency: "EUR"175
details: #{ description: "Some transaction"176
rate: 1.0177
targetCurrency: "EUR" }178
reference-number: "TRF-SAME"179
running-balance: 900.00))180
(result (tally-map-transaction txn))181
(wise-posting (car (journal-transaction-postings result))))182
(assert-false (journal-posting-cost wise-posting)))))184
;; ========== Batch Mapping ==========186
(test-group "tally-map-to-journal"187
(test "maps multiple transactions"188
(let* ((txns (list189
(wise-transaction190
type: "DEBIT"191
date: "2026-03-15T12:00:00Z"192
amount: -50.00193
currency: "EUR"194
details: #{ description: "Purchase 1" }195
reference-number: "TRF-001"196
running-balance: 950.00)197
(wise-transaction198
type: "CREDIT"199
date: "2026-03-16T09:00:00Z"200
amount: 200.00201
currency: "EUR"202
details: #{ description: "Deposit" }203
reference-number: "TRF-002"204
running-balance: 1150.00)))205
(result (tally-map-to-journal txns)))206
(assert-equal 2 (length result))207
(assert-equal "Purchase 1" (journal-transaction-description (car result)))208
(assert-equal "Deposit" (journal-transaction-description (cadr result))))))210
;; ========== Deduplication Integration ==========212
(test-group "deduplication with mapped transactions"213
(test "deduplicates by ref tag from Wise reference number"214
(let* ((existing (list215
(journal-transaction216
date: "2026-03-15"217
status: "*"218
code: "TRF-001"219
description: "Existing purchase"220
tags: (list (cons "ref" "TRF-001"))221
postings: (list222
(journal-posting account: "assets:wise:eur"223
amount: (journal-amount quantity: -50 commodity: "EUR"))224
(journal-posting account: "expenses:uncategorized")))))225
;; Simulate pulling same + new transaction226
(new-wise-txns (list227
(wise-transaction228
type: "DEBIT"229
date: "2026-03-15T12:00:00Z"230
amount: -50.00231
currency: "EUR"232
details: #{ description: "Existing purchase" }233
reference-number: "TRF-001"234
running-balance: 950.00)235
(wise-transaction236
type: "DEBIT"237
date: "2026-03-17T14:00:00Z"238
amount: -30.00239
currency: "EUR"240
details: #{ description: "New purchase" }241
reference-number: "TRF-003"242
running-balance: 920.00)))243
(mapped (tally-map-to-journal new-wise-txns))244
(unique (deduplicate-transactions mapped existing)))245
(assert-equal 1 (length unique))246
(assert-equal "New purchase" (journal-transaction-description (car unique))))))248
;; ========== Journal Output Format ==========250
(test-group "mapped transaction formatting"251
(test "formatted output is valid journal syntax"252
(let* ((txn (wise-transaction253
type: "DEBIT"254
date: "2026-03-15T14:30:00Z"255
amount: -42.99256
currency: "EUR"257
details: #{ description: "Amazon purchase" }258
reference-number: "TRF-12345"259
running-balance: 1234.56))260
(mapped (tally-map-transaction txn))261
(formatted (format-transaction mapped)))262
;; Should start with date263
(assert-true (string-starts-with? formatted "2026-03-15"))264
;; Should contain the Wise account265
(assert-true (string-contains? formatted "assets:wise:eur"))266
;; Should contain the expense account267
(assert-true (string-contains? formatted "expenses:uncategorized"))268
;; Should contain the reference code269
(assert-true (string-contains? formatted "TRF-12345")))))271
(run-tests)