AtlatestRepositorysigil-wise
sigil-wise / tree / srcwise.sgl
1
;;; (wise) - Wise (TransferWise) API client library.2
;;;3
;;; Provides functions for querying profiles, balances, and transaction4
;;; statements from the Wise API using a personal API token.6
(define-library (wise)7
(import (sigil core)8
(sigil dict)9
(sigil string)10
(sigil struct)11
(sigil json)12
(only (sigil array) array-map)13
(only (sigil http) url-encode-value build-query-string)14
(sigil http client)15
(only (sigil time) parse-iso-date format-iso-date16
days-in-month add-days date-compare))18
(export ;; Client19
wise-client20
wise-client?21
wise-client-token22
wise-client-base-url24
;; Records25
wise-profile26
wise-profile?27
wise-profile-id28
wise-profile-type29
wise-profile-name31
wise-balance32
wise-balance?33
wise-balance-id34
wise-balance-currency35
wise-balance-type36
wise-balance-amount37
wise-balance-reserved-amount39
wise-transaction40
wise-transaction?41
wise-transaction-type42
wise-transaction-date43
wise-transaction-amount44
wise-transaction-currency45
wise-transaction-details46
wise-transaction-reference-number47
wise-transaction-running-balance49
;; API functions50
wise-profiles51
wise-balances52
wise-statement53
wise-transactions55
;; Parsing56
parse-profile57
parse-balance58
parse-transaction60
;; Utilities61
wise-date-windows)63
(begin65
;; ---------------------------------------------------------------66
;; Records67
;; ---------------------------------------------------------------69
(define-struct wise-client70
(token)71
(base-url default: "https://api.wise.com"))73
(define-struct wise-profile74
(id)75
(type)76
(name))78
(define-struct wise-balance79
(id)80
(currency)81
(type)82
(amount)83
(reserved-amount))85
(define-struct wise-transaction86
(type)87
(date)88
(amount)89
(currency)90
(details)91
(reference-number)92
(running-balance))94
;; ---------------------------------------------------------------95
;; Internal helpers96
;; ---------------------------------------------------------------98
;;; Build authorization headers for the Wise API.99
(define (wise-auth-headers client)100
#{ authorization: (string-append "Bearer " (wise-client-token client)) })102
;;; Build an API URL from the client base URL and path segments.103
(define (wise-api-url client . parts)104
(apply build-api-url (wise-client-base-url client) parts))107
;;; Response checker with Wise-specific error context.108
(define check-wise-response109
(make-response-checker110
name: "Wise API"111
handlers: (list112
(cons 403 "This may be an SCA restriction for UK/EEA accounts. See https://docs.wise.com for SCA re-authentication.")113
(cons 429 "Rate limited. Retry after a delay."))))115
;;; Perform an authenticated GET request and return parsed JSON.116
(define (wise-get/json client url)117
(check-wise-response118
(http-get url headers: (wise-auth-headers client))))120
;; ---------------------------------------------------------------121
;; Response parsing122
;; ---------------------------------------------------------------124
;;; Parse a profile dict from the API response into a wise-profile record.125
(define (parse-profile data)126
(let ((full-name (dict-ref data fullName: #f))127
(details (dict-ref data details: #f)))128
(wise-profile129
id: (dict-ref data id:)130
type: (dict-ref data type:)131
name: (or full-name132
(if details133
(string-append134
(dict-ref details firstName: "")135
" "136
(dict-ref details lastName: ""))137
"")))))139
;;; Parse a balance dict from the API response into a wise-balance record.140
(define (parse-balance data)141
(let ((amount-obj (dict-ref data amount: #{}))142
(reserved-obj (dict-ref data reservedAmount: #{})))143
(wise-balance144
id: (dict-ref data id:)145
currency: (dict-ref data currency:146
(dict-ref amount-obj currency: ""))147
type: (dict-ref data type: "STANDARD")148
amount: (dict-ref amount-obj value: 0)149
reserved-amount: (dict-ref reserved-obj value: 0))))151
;;; Parse a transaction dict from the API response into a wise-transaction record.152
(define (parse-transaction data)153
(let ((amount-obj (dict-ref data amount: #{}))154
(balance-obj (dict-ref data runningBalance: #{})))155
(wise-transaction156
type: (dict-ref data type:)157
date: (dict-ref data date:)158
amount: (dict-ref amount-obj value: 0)159
currency: (dict-ref amount-obj currency: "")160
details: (dict-ref data details: #{})161
reference-number: (dict-ref data referenceNumber: "")162
running-balance: (dict-ref balance-obj value: 0))))164
;; ---------------------------------------------------------------165
;; Date windowing166
;; ---------------------------------------------------------------168
;;; Maximum number of days per statement request (Wise limit).169
(define wise-max-window-days 469)171
;;; Split a date range into windows of at most wise-max-window-days.172
;;; Returns a list of (start-iso . end-iso) pairs.173
(define (wise-date-windows start-iso end-iso)174
(let ((start (parse-iso-date start-iso))175
(end (parse-iso-date end-iso)))176
(let loop ((current start) (windows '()))177
(if (>= (date-compare current end) 0)178
(reverse windows)179
(let* ((window-end (add-days current wise-max-window-days))180
(actual-end (if (> (date-compare window-end end) 0)181
end182
window-end)))183
(loop actual-end184
(cons (cons (format-iso-date current)185
(format-iso-date actual-end))186
windows)))))))188
;; ---------------------------------------------------------------189
;; API functions190
;; ---------------------------------------------------------------192
;;; Fetch all profiles for the authenticated user.193
;;; Returns a list of wise-profile records.194
(define (wise-profiles client)195
(let ((data (wise-get/json client196
(wise-api-url client "v2" "profiles"))))197
(if (array? data)198
(array->list (array-map parse-profile data))199
'())))201
;;; Fetch balances for a profile.202
;;; Returns a list of wise-balance records.203
(define (wise-balances client profile-id)204
(let ((url (string-append205
(wise-api-url client "v4" "profiles"206
(number->string profile-id)207
"balances")208
"?types=STANDARD")))209
(let ((data (wise-get/json client url)))210
(if (array? data)211
(array->list (array-map parse-balance data))212
'()))))214
;;; Fetch a balance statement for a single balance and date range.215
;;; Automatically splits the range into windows of <= 469 days.216
;;; Returns a list of wise-transaction records.217
(define (wise-statement client profile-id balance-id currency218
interval-start interval-end)219
(let ((windows (wise-date-windows interval-start interval-end))220
(pid (number->string profile-id))221
(bid (number->string balance-id)))222
(let loop ((wins windows) (txns '()))223
(if (null? wins)224
(reverse txns)225
(let* ((win (car wins))226
(url (string-append227
(wise-api-url client "v1" "profiles" pid228
"balance-statements" bid229
"statement.json")230
(build-query-string231
(list (cons "currency" currency)232
(cons "intervalStart" (car win))233
(cons "intervalEnd" (cdr win))234
(cons "type" "COMPACT")))))235
(data (wise-get/json client url))236
(raw-txns (dict-ref data transactions: #[])))237
(loop (cdr wins)238
(append (reverse239
(array->list240
(array-map parse-transaction raw-txns)))241
txns)))))))243
;;; Fetch transactions across all balances for a profile and date range.244
;;; Optionally filter by currency (pass #f for all currencies).245
;;; Returns a list of wise-transaction records.246
(define (wise-transactions client profile-id interval-start interval-end247
. rest)248
(let ((currency-filter (if (null? rest) #f (car rest))))249
(let ((balances (wise-balances client profile-id)))250
(let ((filtered (if currency-filter251
(filter (lambda (b)252
(string=? (wise-balance-currency b)253
currency-filter))254
balances)255
balances)))256
(let loop ((bals filtered) (all-txns '()))257
(if (null? bals)258
(reverse all-txns)259
(let* ((bal (car bals))260
(txns (wise-statement client profile-id261
(wise-balance-id bal)262
(wise-balance-currency bal)263
interval-start interval-end)))264
(loop (cdr bals)265
(append (reverse txns) all-txns)))))))))267
))