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)