-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstarling.el
269 lines (224 loc) · 7.17 KB
/
starling.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
;;; starling.el -- Staling bank info in emacs. -*- lexical-binding: t -*-
;; Copyright (C) 2024 Joe Higton
;; Author: Joe Higton <[email protected]>
;; Version: 0.1.0
;; Homepage: https://github.com/draxil/starling-el
;; Keywords: banking, finance
;; Package-Requires: ((emacs "28") (plz "0.7.2"))"
;;; Commentary:
;;
;; Get info from your starling bank account in Emacs!
;; See the Readme.org for more.
;;
;;; Licence:
;;
;; Please see the LICENCE file.
;;; Code:
(require 'plz)
(require 'auth-source)
(defgroup starling ()
"starling bank module")
(defcustom starling-show-accounts-as-spaces 't
"Show account balances along with spaces."
:type 'boolean
:group 'starling)
;; TODO options for dealing with multiple accounts.
(defun starling--url-base ()
""
"https://api.starlingbank.com/")
(defun starling--url (path)
"build a full url for path"
(concat (starling--url-base) path))
(defun starling--key ()
(let ((key
(auth-source-pick-first-password
:host "api.starlingbank.com"
:user "personal-token")))
(cond
((null key)
(error
"No starling key found, please check the docs for how to configure"))
(t
key))))
(defun starling--headers ()
""
`(("Authorization" . ,(concat "Bearer " (starling--key)))))
(defun starling--get-accounts ()
"get the list of accounts"
(starling--do 'get "api/v2/accounts"))
(defun starling--do (verb path &optional body)
"Call the Starling API, and decode the response from JSON to an alist.
VERB is a HTTP verb, e.g 'get.
PATH is the path (with no leading slash) of the call you want to
make, e.g api/v2/accounts
BODY optional, body to send in the request (TODO, not actually any use for this yet)."
;; TODO: things go wrong!
(let ((req
(plz
verb
(starling--url path)
:headers (starling--headers)
:as #'json-read)))
req))
(defun starling--main-account ()
;; TODO: not fault tolerant, assumes first account is the one!
;; ..which is dumb.
;; TODO: cache?
(let ((accounts (alist-get 'accounts (starling--get-accounts))))
(cond
((arrayp accounts)
(aref accounts 0)))))
(defun starling--main-account-uuid ()
(alist-get 'accountUid (starling--main-account)))
(defun starling--main-account-default-category ()
(alist-get 'defaultCategory (starling--main-account)))
(defun starling--get-spaces ()
"fetch current state of spaces"
(starling--do
'get
(concat
"api/v2/account/" (starling--main-account-uuid) "/spaces")))
(defun starling-space-table ()
;; TODO process all accounts?
(let ((spaces (starling--get-spaces)))
(append
(mapcar
(lambda (space)
(list
(alist-get 'savingsGoalUid space)
(vector
(alist-get 'name space)
(starling--display-cash (alist-get 'totalSaved space)))))
(alist-get 'savingsGoals spaces))
(mapcar
(lambda (space)
(list
(alist-get 'spaceUid space)
(vector
(alist-get 'name space)
(starling--display-cash (alist-get 'balance space)))))
(alist-get 'spendingSpaces spaces))
(when starling-show-accounts-as-spaces
(mapcar
(lambda (balance)
(list
(alist-get 'uuid balance)
(vector
(alist-get 'name balance)
(starling--display-cash (car (alist-get 'balance balance))))))
(starling--account-display-balances))))))
(defun starling--display-cash (cash)
"Display a starling cash value."
;; TODO care for currency?
(starling--to-major (alist-get 'minorUnits cash)))
(defun starling--to-major (units)
"Convert minor UNITS (pence cents) to major (pounds dollars).
Also make it a string, for display purposes."
(format "%.2f" (/ units 100.00)))
;;(/ units 100.00)
(defun starling--account-display-balances ()
"Get account balances."
;; FUTURE: option to pick which balance to display
;; TODO: other accounts?
;; TODO: other name?
(let*
(
(main-uuid (starling--main-account-uuid))
(main-account (starling--do
'get
(concat
"api/v2/accounts/"
main-uuid
"/balance")))
)
`(
(
;; TODO: real name?
(name . "Main account")
(balance . (,(alist-get
'effectiveBalance
main-account
)))
(uuid .
,(starling--main-account-default-category)
)))))
(defvar-keymap starling-spaces-mode-map
:suppress 't
:parent tabulated-list-mode-map
"RET" #'starling--maybe-show-transactions)
(define-derived-mode
starling-spaces-mode
tabulated-list-mode
"starling-spaces-mode"
"Mode for viewing starling spaces."
(setq tabulated-list-format
[("Name" 60 t) ("Amount" 10 t :right-align 't)])
(setq tabulated-list-sort-key '("Name" . nil))
(tabulated-list-init-header))
(defun starling-spaces ()
"Shows the current balances of your Starling Spaces. "
(interactive)
(pop-to-buffer "*Starling Spaces*" nil)
(starling-spaces-mode)
(setq tabulated-list-entries (starling-space-table))
(tabulated-list-print 1)
)
(defun starling--txns-since ()
(format-time-string "%FT00:00:00Z" (- (time-convert (current-time) 'integer) 2592000)))
(defun starling--maybe-show-transactions ()
"Possibly show transactions, if we're on a line with an id."
(interactive)
(when (tabulated-list-get-id)
(starling--show-transactions
(starling--do
'get
;; TODO: sensible date:
(concat "api/v2/feed/account/" (starling--main-account-uuid) "/category/" (tabulated-list-get-id) "?changesSince=" (starling--txns-since)))
)))
(define-derived-mode
starling-transactions-mode
tabulated-list-mode
"starling-transactions-mode"
"Mode for viewing Starling transactions."
;; TODO customisable columns?
(setq tabulated-list-format
[("Who" 20 t) ("Description" 60 t) ("Category" 20 t) ("Amount" 10 t :right-align 't) ("Time" 20 t)])
(tabulated-list-init-header))
(defun starling--show-transactions (txns)
"Show the current balances of your Starling Spaces for TXNS."
;; TODO space name?
(pop-to-buffer "*Starling Trnsactions*" nil)
(starling-transactions-mode)
(setq tabulated-list-entries (starling-transactions--table txns))
(tabulated-list-print 1))
(defun starling-transactions--table (txns)
"Table for starling transactions TXNS."
(mapcar
(lambda (txn)
(list
(alist-get 'feedItemUid txn)
(vector
(alist-get 'counterPartyName txn)
(starling--describe-txn txn)
(starling--format-category (alist-get 'spendingCategory txn))
(starling--txn-amount txn)
(starling--txn-time txn)
)))
(alist-get 'feedItems txns)))
(defun starling--describe-txn (txn)
"Describe a starling transaction TXN."
(concat
(alist-get 'reference txn)
))
(defun starling--txn-amount (txn)
"Present the amount of a transaction TXN."
(concat
(when (equal (alist-get 'direction txn) "OUT") "-")
(starling--display-cash (alist-get 'amount txn))))
(defun starling--txn-time (txn)
"Present the time of a transaction TXN."
(alist-get 'transactionTime txn))
(defun starling--format-category (category)
"Format a starling spending CATEGORY."
(upcase-initials (string-replace "_" " " (downcase category))))
(provide 'starling)