source: docs/P4-Prototype/wiki/UseCase0007Implementation.md

main
Last change on this file was ef1c1c7, checked in by Stefan <trsunovstefan@…>, 6 days ago

Wiki docs, phase 6 and phase 7 added

  • Property mode set to 100644
File size: 10.9 KB
Line 
1= Use-case 0007 Implementation - Manage watchlist =
2
3'''Initiating actor:''' Trader
4
5'''Other actors:''' —
6
7A logged-in Trader keeps a list of crypto assets they want to monitor, with the last
8price of each. The first time the watchlist is opened the system creates a default
9watchlist named "Favorites" for the Trader. From a sub-menu the Trader can list the
10watchlist, add a crypto or remove one. The Trader never types a symbol: for adding, the
11system lists, numbered, only the cryptos that are not on the watchlist yet, and for
12removing, only the cryptos that are on it; the Trader picks one by its number. Adding
13a crypto that is already on the list is a no-op (idempotent), and a number that is not
14in the list is refused without touching the database.
15
16Original use-case description (P3): [wiki:UseCase0007].
17Implementation: `server/watchlist.go`, functions
18`ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and
19`removeFromWatchlist`, with `pickNumber` from
20`server/market.go` (the code is shown at the end of this page).
21
22All statements run on the `project` schema (the connection sets
23`search_path=project,public` in `server/db/db.go`). The SQL below is copied from the
24Go code; only the Go source indentation is removed.
25
26The run shown is user `alice` on the seed data, whose watchlist contains BTC, ETH and
27SOL. She lists it, adds ADA, tries to remove a number that is not in the list, removes
28SOL and lists the result.
29
30== Scenario ==
31
32 1. '''Trader''' chooses `[8] Manage watchlist` in the authenticated menu (types `8`).
33 2. '''System''' makes sure the Trader has a watchlist and takes the id of the oldest one
34 (`ensureDefaultWatchlist`; `$1` = the logged-in user's id):
35
36{{{
37SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1
38}}}
39
40Only if this returns no row, it creates the default watchlist and uses its id:
41
42{{{
43INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id
44}}}
45
46(alice already has the seed watchlist "Favorites", so only the `SELECT` runs.) The
47watchlist id is kept in Go and used as `$1` in all statements below.
48
49 3. '''System''' shows the sub-menu `-- Watchlist --` with `[1] List items`,
50 `[2] Add crypto`, `[3] Remove crypto` and `[0] Back`.
51
52[[Image(uc0007_1_3_menu.png)]]
53
54=== List items ===
55
56 4. '''Trader''' chooses `[1] List items`.
57 5. '''System''' lists the cryptos on the watchlist with their last price against USD
58 (`listWatchlist`; `$1` = watchlist id):
59
60{{{
61SELECT c.symbol, c.name, COALESCE(lp.price, 0)
62 FROM watchlist_items wi
63 JOIN crypto c ON c.id = wi.crypto_id
64 LEFT JOIN markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
65 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
66 WHERE wi.watchlist_id = $1
67 ORDER BY c.symbol
68}}}
69
70For alice it prints `BTC Bitcoin 67140.000000`, `ETH Ethereum 3520.000000` and
71`SOL Solana 166.100000` (an empty watchlist prints `(watchlist is empty)`), then
72shows the sub-menu again.
73
74[[Image(uc0007_list.png)]]
75
76=== Add a crypto ===
77
78 6. '''Trader''' chooses `[2] Add crypto`.
79 7. '''System''' lists, numbered, the cryptos that are not on the watchlist yet
80 (`addToWatchlist`; `$1` = watchlist id):
81
82{{{
83SELECT c.id, c.symbol, c.name
84 FROM crypto c
85 WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
86 WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
87 ORDER BY c.symbol
88}}}
89
90For alice it prints `1 ADA Cardano` and `2 DOGE Dogecoin` and asks
91`Crypto # to add:`. Go keeps each row's crypto id in memory. (If every crypto is
92already on the watchlist, it prints `Every crypto is already on your watchlist.`
93instead.)
94
95[[Image(uc0007_add_1_list.png)]]
96
97 8. '''Trader''' picks the crypto by its number in the list: `1` (ADA).
98 9. '''System''' adds the crypto of row 1 to the watchlist (`$1` = watchlist id,
99 `$2` = the chosen crypto's id); thanks to the unique constraint and
100 `ON CONFLICT ... DO NOTHING`, adding a crypto that is already there changes
101 nothing:
102
103{{{
104INSERT INTO watchlist_items (watchlist_id, crypto_id)
105 VALUES ($1, $2)
106 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING
107}}}
108
109It prints `Added ADA.` and shows the sub-menu again.
110
111[[Image(uc0007_add_2_added.png)]]
112
113=== Remove a crypto ===
114
115 10. '''Trader''' chooses `[3] Remove crypto`.
116 11. '''System''' lists, numbered, the cryptos that are on the watchlist
117 (`removeFromWatchlist`; `$1` = watchlist id):
118
119{{{
120SELECT c.id, c.symbol, c.name
121 FROM watchlist_items wi
122 JOIN crypto c ON c.id = wi.crypto_id
123 WHERE wi.watchlist_id = $1
124 ORDER BY c.symbol
125}}}
126
127For alice it now prints `1 ADA Cardano`, `2 BTC Bitcoin`, `3 ETH Ethereum` and
128`4 SOL Solana` and asks `Crypto # to remove:`. Go keeps each row's crypto id in
129memory. (If the watchlist is empty, it prints `Your watchlist is empty.` instead.)
130
131[[Image(uc0007_remove_1_list.png)]]
132
133 12. '''Trader''' picks the crypto by its number in the list: `4` (SOL).
134 13. '''System''' removes the crypto of row 4 from the watchlist (`$1` = watchlist id,
135 `$2` = the chosen crypto's id):
136
137{{{
138DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2
139}}}
140
141It prints `Removed SOL.` and shows the sub-menu again.
142
143[[Image(uc0007_remove_2_removed.png)]]
144
145==== Alternate flow 12a — number not in the list ====
146
147Before removing SOL, alice first chose `[3] Remove crypto` and, at step 12, entered `5`
148while only numbers 1–4 were listed. `pickNumber` prints
149`Invalid choice, enter a number from 1 to 4.`, the `DELETE` is not run and the sub-menu
150is shown again; she then chose `[3]` once more, which returned the scenario to step 11.
151The same check applies to the number entered at step 8.
152
153[[Image(uc0007_remove_invalid.png)]]
154
155=== Verification — list after the changes ===
156
157Choosing `[1] List items` again runs the query from step 5, which now returns
158`ADA Cardano 0.453750`, `BTC Bitcoin 67140.000000` and `ETH Ethereum 3520.000000`:
159ADA was added and SOL removed. `[0] Back` returns to the authenticated menu.
160
161[[Image(uc0007_list_after.png)]]
162
163== Source code ==
164
165`server/watchlist.go` — `ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and `removeFromWatchlist`:
166
167{{{
168// ManageWatchlist - UC0007
169// Ensures the user has a default watchlist, then allows listing, adding,
170// removing entries.
171func ManageWatchlist(s *Session) {
172 wlID, err := ensureDefaultWatchlist(s.UserID)
173 if err != nil {
174 fmt.Println("Error:", err)
175 return
176 }
177 for {
178 fmt.Println("\n-- Watchlist --")
179 fmt.Println("[1] List items")
180 fmt.Println("[2] Add crypto")
181 fmt.Println("[3] Remove crypto")
182 fmt.Println("[0] Back")
183 switch prompt("> ") {
184 case "1":
185 listWatchlist(wlID)
186 case "2":
187 addToWatchlist(wlID)
188 case "3":
189 removeFromWatchlist(wlID)
190 case "0":
191 return
192 default:
193 fmt.Println("Unknown option.")
194 }
195 }
196}
197
198func ensureDefaultWatchlist(userID string) (string, error) {
199 var id string
200 err := db.DB.QueryRow(
201 `SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1`,
202 userID,
203 ).Scan(&id)
204 if err == sql.ErrNoRows {
205 err = db.DB.QueryRow(
206 `INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id`,
207 userID,
208 ).Scan(&id)
209 return id, err
210 }
211 return id, err
212}
213
214func listWatchlist(wlID string) {
215 rows, err := db.DB.Query(`
216 SELECT c.symbol, c.name, COALESCE(lp.price, 0)
217 FROM watchlist_items wi
218 JOIN crypto c ON c.id = wi.crypto_id
219 LEFT JOIN markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD'
220 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id
221 WHERE wi.watchlist_id = $1
222 ORDER BY c.symbol`, wlID)
223 if err != nil {
224 fmt.Println("Error:", err)
225 return
226 }
227 defer rows.Close()
228
229 fmt.Println()
230 fmt.Printf(" %-8s %-20s %15s\n", "Symbol", "Name", "Last price")
231 fmt.Println(" --------------------------------------------------")
232 empty := true
233 for rows.Next() {
234 var sym, name string
235 var price float64
236 if err := rows.Scan(&sym, &name, &price); err != nil {
237 fmt.Println("scan error:", err)
238 return
239 }
240 fmt.Printf(" %-8s %-20s %15.6f\n", sym, name, price)
241 empty = false
242 }
243 if empty {
244 fmt.Println(" (watchlist is empty)")
245 }
246}
247
248// addToWatchlist lists the cryptos that are not on the watchlist yet,
249// numbered, and adds the one the user picks.
250func addToWatchlist(wlID string) {
251 rows, err := db.DB.Query(`
252 SELECT c.id, c.symbol, c.name
253 FROM crypto c
254 WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi
255 WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id)
256 ORDER BY c.symbol`, wlID)
257 if err != nil {
258 fmt.Println("Error:", err)
259 return
260 }
261 type option struct{ id, symbol, name string }
262 var list []option
263 for rows.Next() {
264 var o option
265 if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
266 rows.Close()
267 fmt.Println("scan error:", err)
268 return
269 }
270 list = append(list, o)
271 }
272 rows.Close()
273 if len(list) == 0 {
274 fmt.Println("Every crypto is already on your watchlist.")
275 return
276 }
277 fmt.Println()
278 fmt.Printf(" %-4s %-8s %s\n", "#", "Symbol", "Name")
279 fmt.Println(" ------------------------------")
280 for i, o := range list {
281 fmt.Printf(" %-4d %-8s %s\n", i+1, o.symbol, o.name)
282 }
283 k, err := pickNumber("Crypto # to add: ", len(list))
284 if err != nil {
285 fmt.Println(err)
286 return
287 }
288 _, err = db.DB.Exec(
289 `INSERT INTO watchlist_items (watchlist_id, crypto_id)
290 VALUES ($1, $2)
291 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING`,
292 wlID, list[k].id,
293 )
294 if err != nil {
295 fmt.Println("Error:", err)
296 return
297 }
298 fmt.Printf("Added %s.\n", list[k].symbol)
299}
300
301// removeFromWatchlist lists the watchlist's cryptos, numbered, and removes
302// the one the user picks.
303func removeFromWatchlist(wlID string) {
304 rows, err := db.DB.Query(`
305 SELECT c.id, c.symbol, c.name
306 FROM watchlist_items wi
307 JOIN crypto c ON c.id = wi.crypto_id
308 WHERE wi.watchlist_id = $1
309 ORDER BY c.symbol`, wlID)
310 if err != nil {
311 fmt.Println("Error:", err)
312 return
313 }
314 type option struct{ id, symbol, name string }
315 var list []option
316 for rows.Next() {
317 var o option
318 if err := rows.Scan(&o.id, &o.symbol, &o.name); err != nil {
319 rows.Close()
320 fmt.Println("scan error:", err)
321 return
322 }
323 list = append(list, o)
324 }
325 rows.Close()
326 if len(list) == 0 {
327 fmt.Println("Your watchlist is empty.")
328 return
329 }
330 fmt.Println()
331 fmt.Printf(" %-4s %-8s %s\n", "#", "Symbol", "Name")
332 fmt.Println(" ------------------------------")
333 for i, o := range list {
334 fmt.Printf(" %-4d %-8s %s\n", i+1, o.symbol, o.name)
335 }
336 k, err := pickNumber("Crypto # to remove: ", len(list))
337 if err != nil {
338 fmt.Println(err)
339 return
340 }
341 if _, err := db.DB.Exec(
342 `DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2`,
343 wlID, list[k].id,
344 ); err != nil {
345 fmt.Println("Error:", err)
346 return
347 }
348 fmt.Printf("Removed %s.\n", list[k].symbol)
349}
350}}}
351
352`server/market.go` — `pickNumber`:
353
354{{{
355// pickNumber reads a 1-based choice from a list of n items.
356func pickNumber(label string, n int) (int, error) {
357 if n == 0 {
358 return 0, fmt.Errorf("Nothing to choose from.")
359 }
360 k, err := strconv.Atoi(prompt(label))
361 if err != nil || k < 1 || k > n {
362 return 0, fmt.Errorf("Invalid choice, enter a number from 1 to %d.", n)
363 }
364 return k - 1, nil
365}
366}}}
Note: See TracBrowser for help on using the repository browser.