AtlatestRepositorytally

tally / tree / testtest-sync.sgl

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.
5
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-transaction
18 type: "DEBIT"
19 date: "2026-03-15T14:30:00Z"
20 amount: -42.99
21 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 deduplication
31 (assert-equal "TRF-12345" (cdr (assoc "ref" (journal-transaction-tags result))))
32 ;; Check postings
33 (let ((postings (journal-transaction-postings result)))
34 (assert-equal 2 (length postings))
35 ;; First posting: wise balance
36 (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 assertion
41 (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-transaction
51 type: "CREDIT"
52 date: "2026-03-20T10:00:00Z"
53 amount: 500.00
54 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 posting
62 (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 posting
66 (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-transaction
72 type: "DEBIT"
73 date: "2026-03-15T12:00:00Z"
74 amount: -15.50
75 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-transaction
85 type: "DEBIT"
86 date: "2026-03-15T12:00:00Z"
87 amount: -10.00
88 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-transaction
100 type: "DEBIT"
101 date: "2026-03-15T12:00:00Z"
102 amount: -10.00
103 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-transaction
116 type: "DEBIT"
117 date: "2026-03-15T12:00:00Z"
118 amount: -500.00
119 currency: "EUR"
120 details: #{ description: "Currency conversion"
121 rate: 1.08
122 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-transaction
136 type: "CREDIT"
137 date: "2026-03-15T12:00:00Z"
138 amount: 540.00
139 currency: "USD"
140 details: #{ description: "Currency conversion"
141 rate: 0.9259
142 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-transaction
157 type: "DEBIT"
158 date: "2026-03-15T12:00:00Z"
159 amount: -42.99
160 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-transaction
171 type: "DEBIT"
172 date: "2026-03-15T12:00:00Z"
173 amount: -100.00
174 currency: "EUR"
175 details: #{ description: "Some transaction"
176 rate: 1.0
177 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 (list
189 (wise-transaction
190 type: "DEBIT"
191 date: "2026-03-15T12:00:00Z"
192 amount: -50.00
193 currency: "EUR"
194 details: #{ description: "Purchase 1" }
195 reference-number: "TRF-001"
196 running-balance: 950.00)
197 (wise-transaction
198 type: "CREDIT"
199 date: "2026-03-16T09:00:00Z"
200 amount: 200.00
201 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 (list
215 (journal-transaction
216 date: "2026-03-15"
217 status: "*"
218 code: "TRF-001"
219 description: "Existing purchase"
220 tags: (list (cons "ref" "TRF-001"))
221 postings: (list
222 (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 transaction
226 (new-wise-txns (list
227 (wise-transaction
228 type: "DEBIT"
229 date: "2026-03-15T12:00:00Z"
230 amount: -50.00
231 currency: "EUR"
232 details: #{ description: "Existing purchase" }
233 reference-number: "TRF-001"
234 running-balance: 950.00)
235 (wise-transaction
236 type: "DEBIT"
237 date: "2026-03-17T14:00:00Z"
238 amount: -30.00
239 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-transaction
253 type: "DEBIT"
254 date: "2026-03-15T14:30:00Z"
255 amount: -42.99
256 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 date
263 (assert-true (string-starts-with? formatted "2026-03-15"))
264 ;; Should contain the Wise account
265 (assert-true (string-contains? formatted "assets:wise:eur"))
266 ;; Should contain the expense account
267 (assert-true (string-contains? formatted "expenses:uncategorized"))
268 ;; Should contain the reference code
269 (assert-true (string-contains? formatted "TRF-12345")))))
271(run-tests)