AtlatestRepositorysigil-wise

sigil-wise / tree / srcwise.sgl

1;;; (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 (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-date
16 days-in-month add-days date-compare))
18 (export ;; Client
19 wise-client
20 wise-client?
21 wise-client-token
22 wise-client-base-url
24 ;; Records
25 wise-profile
26 wise-profile?
27 wise-profile-id
28 wise-profile-type
29 wise-profile-name
31 wise-balance
32 wise-balance?
33 wise-balance-id
34 wise-balance-currency
35 wise-balance-type
36 wise-balance-amount
37 wise-balance-reserved-amount
39 wise-transaction
40 wise-transaction?
41 wise-transaction-type
42 wise-transaction-date
43 wise-transaction-amount
44 wise-transaction-currency
45 wise-transaction-details
46 wise-transaction-reference-number
47 wise-transaction-running-balance
49 ;; API functions
50 wise-profiles
51 wise-balances
52 wise-statement
53 wise-transactions
55 ;; Parsing
56 parse-profile
57 parse-balance
58 parse-transaction
60 ;; Utilities
61 wise-date-windows)
63 (begin
65 ;; ---------------------------------------------------------------
66 ;; Records
67 ;; ---------------------------------------------------------------
69 (define-struct wise-client
70 (token)
71 (base-url default: "https://api.wise.com"))
73 (define-struct wise-profile
74 (id)
75 (type)
76 (name))
78 (define-struct wise-balance
79 (id)
80 (currency)
81 (type)
82 (amount)
83 (reserved-amount))
85 (define-struct wise-transaction
86 (type)
87 (date)
88 (amount)
89 (currency)
90 (details)
91 (reference-number)
92 (running-balance))
94 ;; ---------------------------------------------------------------
95 ;; Internal helpers
96 ;; ---------------------------------------------------------------
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-response
109 (make-response-checker
110 name: "Wise API"
111 handlers: (list
112 (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-response
118 (http-get url headers: (wise-auth-headers client))))
120 ;; ---------------------------------------------------------------
121 ;; Response parsing
122 ;; ---------------------------------------------------------------
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-profile
129 id: (dict-ref data id:)
130 type: (dict-ref data type:)
131 name: (or full-name
132 (if details
133 (string-append
134 (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-balance
144 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-transaction
156 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 windowing
166 ;; ---------------------------------------------------------------
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 end
182 window-end)))
183 (loop actual-end
184 (cons (cons (format-iso-date current)
185 (format-iso-date actual-end))
186 windows)))))))
188 ;; ---------------------------------------------------------------
189 ;; API functions
190 ;; ---------------------------------------------------------------
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 client
196 (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-append
205 (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 currency
218 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-append
227 (wise-api-url client "v1" "profiles" pid
228 "balance-statements" bid
229 "statement.json")
230 (build-query-string
231 (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 (reverse
239 (array->list
240 (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-end
247 . rest)
248 (let ((currency-filter (if (null? rest) #f (car rest))))
249 (let ((balances (wise-balances client profile-id)))
250 (let ((filtered (if currency-filter
251 (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-id
261 (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 ))