Commit7856a485Recorded25 Mar 2026Repositorysigil-wise

Implement Wise API client library

Message

Adds (sigil wise) module with: - Client, profile, balance, and transaction record types - API functions for profiles, balances, and statements - Auto-windowing for the 469-day statement limit - Error handling for 403 SCA and 429 rate limiting - Tests for JSON parsing, date windowing, and record construction

Changed
 .gitignore         |   1 +
 src/sigil/wise.sgl | 395 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/wise-test.sgl | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 644 insertions(+)
Diff
.gitignoreadded
@@ -0,0 +1 @@
+1
build/
src/sigil/wise.sgladded
@@ -0,0 +1,395 @@
+1
;;; (sigil wise) - Wise (TransferWise) API client library.
+2
;;;
+3
;;; Provides functions for querying profiles, balances, and transaction
+4
;;; statements from the Wise API using a personal API token.
+5
+6
(define-library (sigil wise)
+7
(import (sigil core)
+8
(sigil dict)
+9
(sigil string)
+10
(sigil struct)
+11
(sigil json)
+12
(sigil http client))
+13
+14
(export ;; Client
+15
wise-client
+16
wise-client?
+17
wise-client-token
+18
wise-client-base-url
+19
+20
;; Records
+21
wise-profile
+22
wise-profile?
+23
wise-profile-id
+24
wise-profile-type
+25
wise-profile-name
+26
+27
wise-balance
+28
wise-balance?
+29
wise-balance-id
+30
wise-balance-currency
+31
wise-balance-type
+32
wise-balance-amount
+33
wise-balance-reserved-amount
+34
+35
wise-transaction
+36
wise-transaction?
+37
wise-transaction-type
+38
wise-transaction-date
+39
wise-transaction-amount
+40
wise-transaction-currency
+41
wise-transaction-details
+42
wise-transaction-reference-number
+43
wise-transaction-running-balance
+44
+45
;; API functions
+46
wise-profiles
+47
wise-balances
+48
wise-statement
+49
wise-transactions
+50
+51
;; Parsing
+52
parse-profile
+53
parse-balance
+54
parse-transaction
+55
+56
;; Utilities
+57
wise-date-windows)
+58
+59
(begin
+60
+61
;; ---------------------------------------------------------------
+62
;; Records
+63
;; ---------------------------------------------------------------
+64
+65
(define-struct wise-client
+66
(token)
+67
(base-url default: "https://api.wise.com"))
+68
+69
(define-struct wise-profile
+70
(id)
+71
(type)
+72
(name))
+73
+74
(define-struct wise-balance
+75
(id)
+76
(currency)
+77
(type)
+78
(amount)
+79
(reserved-amount))
+80
+81
(define-struct wise-transaction
+82
(type)
+83
(date)
+84
(amount)
+85
(currency)
+86
(details)
+87
(reference-number)
+88
(running-balance))
+89
+90
;; ---------------------------------------------------------------
+91
;; Internal helpers
+92
;; ---------------------------------------------------------------
+93
+94
;;; Build authorization headers for the Wise API.
+95
(define (wise-auth-headers client)
+96
#{ authorization: (string-append "Bearer " (wise-client-token client)) })
+97
+98
;;; Build an API URL from the client base URL and path segments.
+99
(define (wise-api-url client . parts)
+100
(apply string-append (wise-client-base-url client)
+101
(map (lambda (p) (string-append "/" p)) parts)))
+102
+103
;;; URL-encode a query parameter value.
+104
(define (url-encode-value s)
+105
(let ((len (string-length s)))
+106
(let loop ((i 0) (acc '()))
+107
(if (>= i len)
+108
(list->string (reverse acc))
+109
(let ((c (string-ref s i)))
+110
(cond
+111
((or (char-alphabetic? c)
+112
(char-numeric? c)
+113
(char=? c #\-)
+114
(char=? c #\_)
+115
(char=? c #\.)
+116
(char=? c #\~))
+117
(loop (+ i 1) (cons c acc)))
+118
((char=? c #\space)
+119
(loop (+ i 1) (cons #\+ acc)))
+120
(else
+121
(let ((n (char->integer c)))
+122
(loop (+ i 1)
+123
(append (reverse (string->list
+124
(string-append "%"
+125
(if (< n 16) "0" "")
+126
(number->string n 16))))
+127
acc))))))))))
+128
+129
;;; Build a query string from a list of (key . value) pairs.
+130
;;; Pairs with #f values are omitted.
+131
(define (build-query-string params)
+132
(let ((parts (filter (lambda (p) (cdr p)) params)))
+133
(if (null? parts)
+134
""
+135
(string-append "?"
+136
(string-join
+137
(map (lambda (p)
+138
(string-append (car p) "=" (url-encode-value (cdr p))))
+139
parts)
+140
"&")))))
+141
+142
;;; Perform an authenticated GET request and return parsed JSON.
+143
;;; Raises an error on HTTP status >= 400, with Wise-specific context.
+144
(define (wise-get/json client url)
+145
(let ((response (http-get url headers: (wise-auth-headers client))))
+146
(if (not (http-response? response))
+147
(error "Wise API request failed: no response"))
+148
(let ((status (http-response-status response))
+149
(body (http-response-body response)))
+150
(cond
+151
((= status 403)
+152
(error (string-append
+153
"Wise API 403 Forbidden. "
+154
"This may be an SCA restriction for UK/EEA accounts. "
+155
"See https://docs.wise.com for SCA re-authentication. "
+156
"Response: " (or body ""))))
+157
((= status 429)
+158
(error (string-append
+159
"Wise API 429 rate limited. "
+160
"Retry after a delay. "
+161
"Response: " (or body ""))))
+162
((>= status 400)
+163
(error (string-append
+164
"Wise API error " (number->string status) ": "
+165
(or body ""))))
+166
(else
+167
(if (and body (not (string=? body "")))
+168
(json-decode body)
+169
#t))))))
+170
+171
;; ---------------------------------------------------------------
+172
;; Response parsing
+173
;; ---------------------------------------------------------------
+174
+175
;;; Parse a profile dict from the API response into a wise-profile record.
+176
(define (parse-profile data)
+177
(let ((full-name (dict-ref data fullName: #f))
+178
(details (dict-ref data details: #f)))
+179
(wise-profile
+180
id: (dict-ref data id:)
+181
type: (dict-ref data type:)
+182
name: (or full-name
+183
(if details
+184
(string-append
+185
(dict-ref details firstName: "")
+186
" "
+187
(dict-ref details lastName: ""))
+188
"")))))
+189
+190
;;; Parse a balance dict from the API response into a wise-balance record.
+191
(define (parse-balance data)
+192
(let ((amount-obj (dict-ref data amount: #{}))
+193
(reserved-obj (dict-ref data reservedAmount: #{})))
+194
(wise-balance
+195
id: (dict-ref data id:)
+196
currency: (dict-ref amount-obj currency: "")
+197
type: (dict-ref data type: "STANDARD")
+198
amount: (dict-ref amount-obj value: 0)
+199
reserved-amount: (dict-ref reserved-obj value: 0))))
+200
+201
;;; Parse a transaction dict from the API response into a wise-transaction record.
+202
(define (parse-transaction data)
+203
(let ((amount-obj (dict-ref data amount: #{}))
+204
(balance-obj (dict-ref data runningBalance: #{})))
+205
(wise-transaction
+206
type: (dict-ref data type:)
+207
date: (dict-ref data date:)
+208
amount: (dict-ref amount-obj value: 0)
+209
currency: (dict-ref amount-obj currency: "")
+210
details: (dict-ref data details: #{})
+211
reference-number: (dict-ref data referenceNumber: "")
+212
running-balance: (dict-ref balance-obj value: 0))))
+213
+214
;; ---------------------------------------------------------------
+215
;; Date windowing
+216
;; ---------------------------------------------------------------
+217
+218
;;; Maximum number of days per statement request (Wise limit).
+219
(define wise-max-window-days 469)
+220
+221
;;; Parse an ISO 8601 date string "YYYY-MM-DDTHH:MM:SSZ" into a list
+222
;;; of (year month day hour minute second).
+223
(define (parse-iso-date s)
+224
(list (string->number (substring s 0 4))
+225
(string->number (substring s 5 7))
+226
(string->number (substring s 8 10))
+227
(if (> (string-length s) 10)
+228
(string->number (substring s 11 13))
+229
0)
+230
(if (> (string-length s) 13)
+231
(string->number (substring s 14 16))
+232
0)
+233
(if (> (string-length s) 16)
+234
(string->number (substring s 17 19))
+235
0)))
+236
+237
;;; Format a date list back to ISO 8601 UTC string.
+238
(define (format-iso-date parts)
+239
(let ((year (list-ref parts 0))
+240
(month (list-ref parts 1))
+241
(day (list-ref parts 2))
+242
(hour (list-ref parts 3))
+243
(minute (list-ref parts 4))
+244
(second (list-ref parts 5)))
+245
(string-append
+246
(number->string year) "-"
+247
(if (< month 10) "0" "") (number->string month) "-"
+248
(if (< day 10) "0" "") (number->string day) "T"
+249
(if (< hour 10) "0" "") (number->string hour) ":"
+250
(if (< minute 10) "0" "") (number->string minute) ":"
+251
(if (< second 10) "0" "") (number->string second) "Z")))
+252
+253
;;; Days in a given month (handles leap years).
+254
(define (days-in-month year month)
+255
(cond
+256
((memv month '(1 3 5 7 8 10 12)) 31)
+257
((memv month '(4 6 9 11)) 30)
+258
((= month 2)
+259
(if (and (= (modulo year 4) 0)
+260
(or (not (= (modulo year 100) 0))
+261
(= (modulo year 400) 0)))
+262
29 28))
+263
(else 30)))
+264
+265
;;; Add a number of days to a date list, returning a new date list.
+266
(define (add-days parts n)
+267
(let ((year (list-ref parts 0))
+268
(month (list-ref parts 1))
+269
(day (list-ref parts 2))
+270
(hour (list-ref parts 3))
+271
(minute (list-ref parts 4))
+272
(second (list-ref parts 5)))
+273
(let loop ((y year) (m month) (d (+ day n)))
+274
(let ((dim (days-in-month y m)))
+275
(cond
+276
((> d dim)
+277
(let ((new-m (+ m 1)))
+278
(if (> new-m 12)
+279
(loop (+ y 1) 1 (- d dim))
+280
(loop y new-m (- d dim)))))
+281
((< d 1)
+282
(let ((prev-m (- m 1)))
+283
(if (< prev-m 1)
+284
(let ((py (- y 1)))
+285
(loop py 12 (+ d (days-in-month py 12))))
+286
(loop y prev-m (+ d (days-in-month y prev-m))))))
+287
(else
+288
(list y m d hour minute second)))))))
+289
+290
;;; Compare two date lists. Returns -1, 0, or 1.
+291
(define (date-compare a b)
+292
(let loop ((as a) (bs b))
+293
(cond
+294
((null? as) 0)
+295
((< (car as) (car bs)) -1)
+296
((> (car as) (car bs)) 1)
+297
(else (loop (cdr as) (cdr bs))))))
+298
+299
;;; Split a date range into windows of at most wise-max-window-days.
+300
;;; Returns a list of (start-iso . end-iso) pairs.
+301
(define (wise-date-windows start-iso end-iso)
+302
(let ((start (parse-iso-date start-iso))
+303
(end (parse-iso-date end-iso)))
+304
(let loop ((current start) (windows '()))
+305
(if (>= (date-compare current end) 0)
+306
(reverse windows)
+307
(let* ((window-end (add-days current wise-max-window-days))
+308
(actual-end (if (> (date-compare window-end end) 0)
+309
end
+310
window-end)))
+311
(loop actual-end
+312
(cons (cons (format-iso-date current)
+313
(format-iso-date actual-end))
+314
windows)))))))
+315
+316
;; ---------------------------------------------------------------
+317
;; API functions
+318
;; ---------------------------------------------------------------
+319
+320
;;; Fetch all profiles for the authenticated user.
+321
;;; Returns a list of wise-profile records.
+322
(define (wise-profiles client)
+323
(let ((data (wise-get/json client
+324
(wise-api-url client "v2" "profiles"))))
+325
(if (array? data)
+326
(array->list (array-map parse-profile data))
+327
'())))
+328
+329
;;; Fetch balances for a profile.
+330
;;; Returns a list of wise-balance records.
+331
(define (wise-balances client profile-id)
+332
(let ((url (string-append
+333
(wise-api-url client "v4" "profiles"
+334
(number->string profile-id)
+335
"balances")
+336
"?types=STANDARD")))
+337
(let ((data (wise-get/json client url)))
+338
(if (array? data)
+339
(array->list (array-map parse-balance data))
+340
'()))))
+341
+342
;;; Fetch a balance statement for a single balance and date range.
+343
;;; Automatically splits the range into windows of <= 469 days.
+344
;;; Returns a list of wise-transaction records.
+345
(define (wise-statement client profile-id balance-id currency
+346
interval-start interval-end)
+347
(let ((windows (wise-date-windows interval-start interval-end))
+348
(pid (number->string profile-id))
+349
(bid (number->string balance-id)))
+350
(let loop ((wins windows) (txns '()))
+351
(if (null? wins)
+352
(reverse txns)
+353
(let* ((win (car wins))
+354
(url (string-append
+355
(wise-api-url client "v1" "profiles" pid
+356
"balance-statements" bid
+357
"statement.json")
+358
(build-query-string
+359
(list (cons "currency" currency)
+360
(cons "intervalStart" (car win))
+361
(cons "intervalEnd" (cdr win))
+362
(cons "type" "COMPACT")))))
+363
(data (wise-get/json client url))
+364
(raw-txns (dict-ref data transactions: #[])))
+365
(loop (cdr wins)
+366
(append (reverse
+367
(array->list
+368
(array-map parse-transaction raw-txns)))
+369
txns)))))))
+370
+371
;;; Fetch transactions across all balances for a profile and date range.
+372
;;; Optionally filter by currency (pass #f for all currencies).
+373
;;; Returns a list of wise-transaction records.
+374
(define (wise-transactions client profile-id interval-start interval-end
+375
. rest)
+376
(let ((currency-filter (if (null? rest) #f (car rest))))
+377
(let ((balances (wise-balances client profile-id)))
+378
(let ((filtered (if currency-filter
+379
(filter (lambda (b)
+380
(string=? (wise-balance-currency b)
+381
currency-filter))
+382
balances)
+383
balances)))
+384
(let loop ((bals filtered) (all-txns '()))
+385
(if (null? bals)
+386
(reverse all-txns)
+387
(let* ((bal (car bals))
+388
(txns (wise-statement client profile-id
+389
(wise-balance-id bal)
+390
(wise-balance-currency bal)
+391
interval-start interval-end)))
+392
(loop (cdr bals)
+393
(append (reverse txns) all-txns)))))))))
+394
+395
))
test/wise-test.sgladded
@@ -0,0 +1,248 @@
+1
;;; Tests for (sigil wise)
+2
+3
(import (sigil core)
+4
(sigil dict)
+5
(sigil string)
+6
(sigil struct)
+7
(sigil json)
+8
(sigil test)
+9
(sigil wise))
+10
+11
;; ---------------------------------------------------------------
+12
;; Test fixtures — JSON response samples
+13
;; ---------------------------------------------------------------
+14
+15
(define profiles-json
+16
(json-decode
+17
(string-append
+18
"[{\"id\": 12345, \"type\": \"PERSONAL\","
+19
" \"fullName\": \"Alice Smith\","
+20
" \"details\": {\"firstName\": \"Alice\","
+21
" \"lastName\": \"Smith\"}},"
+22
" {\"id\": 67890, \"type\": \"BUSINESS\","
+23
" \"fullName\": \"Acme Corp\","
+24
" \"details\": {\"firstName\": \"Acme\","
+25
" \"lastName\": \"Corp\"}}]")))
+26
+27
(define balances-json
+28
(json-decode
+29
(string-append
+30
"[{\"id\": 100, \"type\": \"STANDARD\","
+31
" \"amount\": {\"value\": 1500.42, \"currency\": \"EUR\"},"
+32
" \"reservedAmount\": {\"value\": 10.00,"
+33
" \"currency\": \"EUR\"}},"
+34
" {\"id\": 200, \"type\": \"STANDARD\","
+35
" \"amount\": {\"value\": 3200.00, \"currency\": \"USD\"},"
+36
" \"reservedAmount\": {\"value\": 0,"
+37
" \"currency\": \"USD\"}}]")))
+38
+39
(define statement-json
+40
(json-decode
+41
(string-append
+42
"{\"accountHolder\": {\"type\": \"PERSONAL\"},"
+43
" \"transactions\": ["
+44
" {\"type\": \"CREDIT\","
+45
" \"date\": \"2026-03-01T10:30:00Z\","
+46
" \"amount\": {\"value\": 500.00,"
+47
" \"currency\": \"EUR\"},"
+48
" \"details\": {\"description\": \"Salary\"},"
+49
" \"referenceNumber\": \"TXN-001\","
+50
" \"runningBalance\": {\"value\": 1500.00,"
+51
" \"currency\": \"EUR\"}},"
+52
" {\"type\": \"DEBIT\","
+53
" \"date\": \"2026-03-05T14:00:00Z\","
+54
" \"amount\": {\"value\": -42.50,"
+55
" \"currency\": \"EUR\"},"
+56
" \"details\": {\"description\": \"Coffee shop\","
+57
" \"merchant\": \"Bean Co\"},"
+58
" \"referenceNumber\": \"TXN-002\","
+59
" \"runningBalance\": {\"value\": 1457.50,"
+60
" \"currency\": \"EUR\"}}"
+61
" ],"
+62
" \"endOfStatementBalance\":"
+63
" {\"value\": 1457.50, \"currency\": \"EUR\"}}")))
+64
+65
;; ---------------------------------------------------------------
+66
;; Profile parsing tests
+67
;; ---------------------------------------------------------------
+68
+69
(test-group "profile parsing"
+70
+71
(test "parse personal profile"
+72
(let ((data (array-ref profiles-json 0)))
+73
(let ((p (parse-profile data)))
+74
(assert-true (wise-profile? p))
+75
(assert-equal 12345 (wise-profile-id p))
+76
(assert-equal "PERSONAL" (wise-profile-type p))
+77
(assert-equal "Alice Smith" (wise-profile-name p)))))
+78
+79
(test "parse business profile"
+80
(let ((data (array-ref profiles-json 1)))
+81
(let ((p (parse-profile data)))
+82
(assert-true (wise-profile? p))
+83
(assert-equal 67890 (wise-profile-id p))
+84
(assert-equal "BUSINESS" (wise-profile-type p))
+85
(assert-equal "Acme Corp" (wise-profile-name p))))))
+86
+87
;; ---------------------------------------------------------------
+88
;; Balance parsing tests
+89
;; ---------------------------------------------------------------
+90
+91
(test-group "balance parsing"
+92
+93
(test "parse EUR balance"
+94
(let ((data (array-ref balances-json 0)))
+95
(let ((b (parse-balance data)))
+96
(assert-true (wise-balance? b))
+97
(assert-equal 100 (wise-balance-id b))
+98
(assert-equal "EUR" (wise-balance-currency b))
+99
(assert-equal "STANDARD" (wise-balance-type b))
+100
(assert-equal 1500.42 (wise-balance-amount b))
+101
(assert-equal 10.00 (wise-balance-reserved-amount b)))))
+102
+103
(test "parse USD balance"
+104
(let ((data (array-ref balances-json 1)))
+105
(let ((b (parse-balance data)))
+106
(assert-equal 200 (wise-balance-id b))
+107
(assert-equal "USD" (wise-balance-currency b))
+108
(assert-equal 3200.00 (wise-balance-amount b))
+109
(assert-equal 0 (wise-balance-reserved-amount b))))))
+110
+111
;; ---------------------------------------------------------------
+112
;; Transaction parsing tests
+113
;; ---------------------------------------------------------------
+114
+115
(test-group "transaction parsing"
+116
+117
(test "parse credit transaction"
+118
(let ((txns (dict-ref statement-json transactions:)))
+119
(let ((t (parse-transaction (array-ref txns 0))))
+120
(assert-true (wise-transaction? t))
+121
(assert-equal "CREDIT" (wise-transaction-type t))
+122
(assert-equal "2026-03-01T10:30:00Z" (wise-transaction-date t))
+123
(assert-equal 500.00 (wise-transaction-amount t))
+124
(assert-equal "EUR" (wise-transaction-currency t))
+125
(assert-equal "TXN-001" (wise-transaction-reference-number t))
+126
(assert-equal 1500.00 (wise-transaction-running-balance t)))))
+127
+128
(test "parse debit transaction"
+129
(let ((txns (dict-ref statement-json transactions:)))
+130
(let ((t (parse-transaction (array-ref txns 1))))
+131
(assert-equal "DEBIT" (wise-transaction-type t))
+132
(assert-equal -42.50 (wise-transaction-amount t))
+133
(assert-equal "TXN-002" (wise-transaction-reference-number t))
+134
(assert-equal 1457.50 (wise-transaction-running-balance t)))))
+135
+136
(test "parse transaction details"
+137
(let ((txns (dict-ref statement-json transactions:)))
+138
(let ((t (parse-transaction (array-ref txns 1))))
+139
(let ((details (wise-transaction-details t)))
+140
(assert-true (dict? details))
+141
(assert-equal "Coffee shop" (dict-ref details description:))
+142
(assert-equal "Bean Co" (dict-ref details merchant:)))))))
+143
+144
;; ---------------------------------------------------------------
+145
;; Date windowing tests
+146
;; ---------------------------------------------------------------
+147
+148
(test-group "date windowing"
+149
+150
(test "single window for short range"
+151
(let ((windows (wise-date-windows
+152
"2026-01-01T00:00:00Z"
+153
"2026-03-01T00:00:00Z")))
+154
(assert-equal 1 (length windows))
+155
(assert-equal "2026-01-01T00:00:00Z" (car (car windows)))
+156
(assert-equal "2026-03-01T00:00:00Z" (cdr (car windows)))))
+157
+158
(test "single window for exactly 469 days"
+159
(let ((windows (wise-date-windows
+160
"2025-01-01T00:00:00Z"
+161
"2026-04-15T00:00:00Z")))
+162
;; 469 days from 2025-01-01 = 2026-04-15
+163
(assert-equal 1 (length windows))))
+164
+165
(test "two windows for range > 469 days"
+166
(let ((windows (wise-date-windows
+167
"2024-01-01T00:00:00Z"
+168
"2026-03-25T00:00:00Z")))
+169
;; ~815 days, needs 2 windows
+170
(assert-true (>= (length windows) 2))
+171
;; First window starts at the start date
+172
(assert-equal "2024-01-01T00:00:00Z" (car (car windows)))
+173
;; Last window ends at the end date
+174
(assert-equal "2026-03-25T00:00:00Z"
+175
(cdr (list-ref windows (- (length windows) 1))))))
+176
+177
(test "three windows for very long range"
+178
(let ((windows (wise-date-windows
+179
"2023-01-01T00:00:00Z"
+180
"2026-03-25T00:00:00Z")))
+181
;; ~1179 days, needs 3 windows
+182
(assert-true (>= (length windows) 3))
+183
;; Windows are contiguous
+184
(assert-equal (cdr (car windows))
+185
(car (list-ref windows 1)))))
+186
+187
(test "empty range returns empty"
+188
(let ((windows (wise-date-windows
+189
"2026-03-01T00:00:00Z"
+190
"2026-03-01T00:00:00Z")))
+191
(assert-equal 0 (length windows))))
+192
+193
(test "preserves time components"
+194
(let ((windows (wise-date-windows
+195
"2026-01-15T08:30:45Z"
+196
"2026-06-20T16:00:00Z")))
+197
(assert-equal 1 (length windows))
+198
(assert-equal "2026-01-15T08:30:45Z" (car (car windows)))
+199
(assert-equal "2026-06-20T16:00:00Z" (cdr (car windows))))))
+200
+201
;; ---------------------------------------------------------------
+202
;; Client construction tests
+203
;; ---------------------------------------------------------------
+204
+205
(test-group "client construction"
+206
+207
(test "default base URL"
+208
(let ((c (wise-client token: "test-token")))
+209
(assert-true (wise-client? c))
+210
(assert-equal "test-token" (wise-client-token c))
+211
(assert-equal "https://api.wise.com" (wise-client-base-url c))))
+212
+213
(test "custom base URL for sandbox"
+214
(let ((c (wise-client token: "sandbox-token"
+215
base-url: "https://api.wise-sandbox.com")))
+216
(assert-equal "https://api.wise-sandbox.com"
+217
(wise-client-base-url c)))))
+218
+219
;; ---------------------------------------------------------------
+220
;; Record construction tests
+221
;; ---------------------------------------------------------------
+222
+223
(test-group "record construction"
+224
+225
(test "wise-profile fields"
+226
(let ((p (wise-profile id: 1 type: "PERSONAL" name: "Test")))
+227
(assert-equal 1 (wise-profile-id p))
+228
(assert-equal "PERSONAL" (wise-profile-type p))
+229
(assert-equal "Test" (wise-profile-name p))))
+230
+231
(test "wise-balance fields"
+232
(let ((b (wise-balance id: 10 currency: "GBP" type: "STANDARD"
+233
amount: 999.99 reserved-amount: 5.00)))
+234
(assert-equal 10 (wise-balance-id b))
+235
(assert-equal "GBP" (wise-balance-currency b))
+236
(assert-equal 999.99 (wise-balance-amount b))
+237
(assert-equal 5.00 (wise-balance-reserved-amount b))))
+238
+239
(test "wise-transaction fields"
+240
(let ((t (wise-transaction type: "CREDIT" date: "2026-01-01T00:00:00Z"
+241
amount: 100.00 currency: "EUR"
+242
details: #{} reference-number: "REF-1"
+243
running-balance: 500.00)))
+244
(assert-equal "CREDIT" (wise-transaction-type t))
+245
(assert-equal 100.00 (wise-transaction-amount t))
+246
(assert-equal "REF-1" (wise-transaction-reference-number t)))))
+247
+248
(run-tests)