| | 1 | = Use-case 0003 Implementation - Deposit virtual funds = |
| | 2 | |
| | 3 | '''Initiating actor:''' Trader |
| | 4 | |
| | 5 | '''Other actors:''' — |
| | 6 | |
| | 7 | A logged-in Trader tops up their virtual cash balance in USD. This is a |
| | 8 | simulation-only operation: no real money changes hands, the amount is simply added to |
| | 9 | the Trader's `available_balance`. Every deposit is also recorded in the ledger |
| | 10 | (`transactions`) so that it appears in the transaction history |
| | 11 | ([wiki:UseCase0006Implementation UseCase0006]). The operation writes to two tables — |
| | 12 | the user row and the ledger — inside a single database transaction, so either both |
| | 13 | changes are stored or neither is. Non-numeric, zero or negative amounts are rejected |
| | 14 | before the database is touched. |
| | 15 | |
| | 16 | Original use-case description (P3): [wiki:UseCase0003]. |
| | 17 | Implementation: `server/account.go`, function `Deposit`; the verification uses |
| | 18 | `ShowBalance` from the same file (the code is shown at the end of this page). |
| | 19 | |
| | 20 | Precondition: the Trader is logged in ([wiki:UseCase0002Implementation UseCase0002]); |
| | 21 | in the run below as `alice`, who starts from the seed state (available 8250.00 USD, |
| | 22 | invested 1750.00 USD). |
| | 23 | |
| | 24 | == Scenario == |
| | 25 | |
| | 26 | 1. '''Trader''' chooses `[2] Deposit virtual funds` in the authenticated menu |
| | 27 | (types `2`). |
| | 28 | 2. '''System''' prints `-- Deposit virtual funds --` and asks `Amount (USD):`. |
| | 29 | |
| | 30 | The screenshot shows steps 1–2: the login as alice, the authenticated menu, the |
| | 31 | choice `2` and the amount prompt waiting for input. |
| | 32 | |
| | 33 | [[Image(uc0003_1_2_deposit.png)]] |
| | 34 | |
| | 35 | 3. '''Trader''' enters an amount: `500`. |
| | 36 | 4. '''System''' validates the input in Go, without accessing the database: the text |
| | 37 | must parse as a number (`strconv.ParseFloat`) and be greater than 0 (see |
| | 38 | alternate flow 4a). |
| | 39 | 5. '''System''' opens one database transaction (`db.DB.Begin()`), increments the |
| | 40 | balance, writes the ledger row and commits. Both statements run in this single |
| | 41 | transaction; if either fails, the deferred `tx.Rollback()` undoes everything. |
| | 42 | `BEGIN` and `COMMIT` are issued by Go's `Begin()` / `Commit()`; the two |
| | 43 | statements are sent exactly as in the code: |
| | 44 | |
| | 45 | {{{ |
| | 46 | BEGIN; |
| | 47 | |
| | 48 | UPDATE users |
| | 49 | SET available_balance = available_balance + $1, |
| | 50 | updated_at = now() |
| | 51 | WHERE id = $2; |
| | 52 | |
| | 53 | INSERT INTO transactions (user_id, type, amount, currency, description) |
| | 54 | VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit'); |
| | 55 | |
| | 56 | COMMIT; |
| | 57 | }}} |
| | 58 | |
| | 59 | Parameters: placeholders are numbered per statement. In the `UPDATE`, `$1` is the |
| | 60 | amount (`500`) and `$2` the user id (`amt, s.UserID`); in the `INSERT` it is the |
| | 61 | other way round, `$1` is the user id and `$2` the amount (`s.UserID, amt`), |
| | 62 | matching the column order. |
| | 63 | |
| | 64 | 6. '''System''' confirms `Deposited 500.0000 USD.` and returns to the authenticated |
| | 65 | menu. |
| | 66 | |
| | 67 | The screenshot shows steps 3–6: the entered amount `500`, the confirmation and the |
| | 68 | authenticated menu again. |
| | 69 | |
| | 70 | [[Image(uc0003_3_6_deposited.png)]] |
| | 71 | |
| | 72 | The statements run on the `project` schema (the connection sets |
| | 73 | `search_path=project,public`), so `users` and `transactions` mean `project.users` |
| | 74 | and `project.transactions`. |
| | 75 | |
| | 76 | === Alternate flow 4a — invalid amount === |
| | 77 | |
| | 78 | If the amount is not a number, or is zero or negative, the system prints |
| | 79 | `Invalid amount.`; no transaction is started and nothing is written. In the run the |
| | 80 | Trader first entered `-50`. In the prototype the system then shows the |
| | 81 | authenticated menu again and the Trader chooses `[2] Deposit virtual funds` once |
| | 82 | more, which returns the scenario to step 2. |
| | 83 | |
| | 84 | [[Image(uc0003_4a_invalid.png)]] |
| | 85 | |
| | 86 | == Verification == |
| | 87 | |
| | 88 | Right after the deposit the Trader chooses `[1] View balance` (function |
| | 89 | `ShowBalance`), which runs (`$1` = user id): |
| | 90 | |
| | 91 | {{{ |
| | 92 | SELECT available_balance, invested_balance FROM users WHERE id = $1 |
| | 93 | }}} |
| | 94 | |
| | 95 | It prints `Available: 8750.0000 USD`, `Invested : 1750.0000 USD`, |
| | 96 | `Total : 10500.0000 USD`. The available balance grew from the seed value 8250.00 |
| | 97 | by exactly the 500.00 deposited (the rejected `-50` changed nothing), and |
| | 98 | `invested_balance` is untouched. |
| | 99 | |
| | 100 | [[Image(uc0003_verify_balance.png)]] |
| | 101 | |
| | 102 | == How to reproduce == |
| | 103 | |
| | 104 | {{{ |
| | 105 | ./eduberza -init |
| | 106 | ./eduberza |
| | 107 | # [2] Login: alice / test123 |
| | 108 | # [2] Deposit virtual funds: -50 -> Invalid amount. |
| | 109 | # [2] Deposit virtual funds: 500 -> Deposited 500.0000 USD. |
| | 110 | # [1] View balance -> Available: 8750.0000 USD |
| | 111 | }}} |
| | 112 | |
| | 113 | All four screenshots come from one real run of exactly these inputs. |
| | 114 | |
| | 115 | == Source code == |
| | 116 | |
| | 117 | `server/account.go` — `Deposit`, and `ShowBalance` used for the verification: |
| | 118 | |
| | 119 | {{{ |
| | 120 | // ShowBalance prints the logged-in user's balances. |
| | 121 | func ShowBalance(s *Session) { |
| | 122 | var avail, invested float64 |
| | 123 | err := db.DB.QueryRow( |
| | 124 | `SELECT available_balance, invested_balance FROM users WHERE id = $1`, |
| | 125 | s.UserID, |
| | 126 | ).Scan(&avail, &invested) |
| | 127 | if err != nil { |
| | 128 | fmt.Println("Error:", err) |
| | 129 | return |
| | 130 | } |
| | 131 | fmt.Printf("\n Available: %.4f USD\n", avail) |
| | 132 | fmt.Printf(" Invested : %.4f USD\n", invested) |
| | 133 | fmt.Printf(" Total : %.4f USD\n", avail+invested) |
| | 134 | } |
| | 135 | |
| | 136 | // Deposit - UC0003 |
| | 137 | // Transactional: updates users.available_balance and inserts a ledger row. |
| | 138 | func Deposit(s *Session) { |
| | 139 | fmt.Println("\n-- Deposit virtual funds --") |
| | 140 | amtStr := prompt("Amount (USD): ") |
| | 141 | amt, err := strconv.ParseFloat(amtStr, 64) |
| | 142 | if err != nil || amt <= 0 { |
| | 143 | fmt.Println("Invalid amount.") |
| | 144 | return |
| | 145 | } |
| | 146 | |
| | 147 | tx, err := db.DB.Begin() |
| | 148 | if err != nil { |
| | 149 | fmt.Println("Error:", err) |
| | 150 | return |
| | 151 | } |
| | 152 | defer tx.Rollback() |
| | 153 | |
| | 154 | if _, err := tx.Exec( |
| | 155 | `UPDATE users |
| | 156 | SET available_balance = available_balance + $1, |
| | 157 | updated_at = now() |
| | 158 | WHERE id = $2`, |
| | 159 | amt, s.UserID, |
| | 160 | ); err != nil { |
| | 161 | fmt.Println("Error:", err) |
| | 162 | return |
| | 163 | } |
| | 164 | if _, err := tx.Exec( |
| | 165 | `INSERT INTO transactions (user_id, type, amount, currency, description) |
| | 166 | VALUES ($1, 'deposit', $2, 'USD', 'Virtual deposit')`, |
| | 167 | s.UserID, amt, |
| | 168 | ); err != nil { |
| | 169 | fmt.Println("Error:", err) |
| | 170 | return |
| | 171 | } |
| | 172 | if err := tx.Commit(); err != nil { |
| | 173 | fmt.Println("Error:", err) |
| | 174 | return |
| | 175 | } |
| | 176 | fmt.Printf("Deposited %.4f USD.\n", amt) |
| | 177 | } |
| | 178 | }}} |