- Timestamp:
- 09/24/26 17:43:19 (6 days ago)
- Branches:
- main
- Children:
- 0cee8ec
- Parents:
- a531b45
- Location:
- docs
- Files:
-
- 64 added
- 6 deleted
- 20 edited
-
P0-ProjectDefinition/wiki/About.md (added)
-
P0-ProjectDefinition/wiki/WikiStart.md (added)
-
P1-ConceptualModel/ERModel.md (modified) (7 diffs)
-
P1-ConceptualModel/ERModelAIUsage.md (modified) (1 diff)
-
P1-ConceptualModel/ERModel_v04.png (added)
-
P1-ConceptualModel/ERModel_v04.xml (added)
-
P1-ConceptualModel/wiki/ERModel.md (added)
-
P1-ConceptualModel/wiki/ERModelAIUsage.md (added)
-
P2-RelationalDesign/RelationalDesign.md (modified) (1 diff)
-
P2-RelationalDesign/relational_schema_v3.png (added)
-
P2-RelationalDesign/wiki/RelationalDesign.md (added)
-
P2-RelationalDesign/wiki/RelationalDesignAIUsage.md (added)
-
P3-UseCaseModel/UseCase0004.md (modified) (3 diffs)
-
P3-UseCaseModel/UseCase0005.md (modified) (1 diff)
-
P3-UseCaseModel/UseCase0007.md (modified) (2 diffs)
-
P3-UseCaseModel/wiki/UseCase0001.md (added)
-
P3-UseCaseModel/wiki/UseCase0002.md (added)
-
P3-UseCaseModel/wiki/UseCase0003.md (added)
-
P3-UseCaseModel/wiki/UseCase0004.md (added)
-
P3-UseCaseModel/wiki/UseCase0005.md (added)
-
P3-UseCaseModel/wiki/UseCase0006.md (added)
-
P3-UseCaseModel/wiki/UseCase0007.md (added)
-
P3-UseCaseModel/wiki/UseCaseModel.md (added)
-
P3-UseCaseModel/wiki/UseCaseModelAIUsage.md (added)
-
P4-Prototype/BuildInstructions.md (modified) (8 diffs)
-
P4-Prototype/PrototypeImplementation.md (modified) (1 diff)
-
P4-Prototype/PrototypeImplementationAIUsage.md (modified) (4 diffs)
-
P4-Prototype/UseCase0001Implementation.md (modified) (2 diffs)
-
P4-Prototype/UseCase0002Implementation.md (modified) (1 diff)
-
P4-Prototype/UseCase0003Implementation.md (modified) (1 diff)
-
P4-Prototype/UseCase0004Implementation.md (modified) (1 diff)
-
P4-Prototype/UseCase0005Implementation.md (modified) (1 diff)
-
P4-Prototype/UseCase0006Implementation.md (modified) (2 diffs)
-
P4-Prototype/UseCase0007Implementation.md (modified) (1 diff)
-
P4-Prototype/screenshots/uc0001_1_register.png (added)
-
P4-Prototype/screenshots/uc0001_3_7_created.png (added)
-
P4-Prototype/screenshots/uc0001_3a_invalid_email.png (added)
-
P4-Prototype/screenshots/uc0001_5a_duplicate.png (added)
-
P4-Prototype/screenshots/uc0001_register.png (deleted)
-
P4-Prototype/screenshots/uc0002_1_login.png (added)
-
P4-Prototype/screenshots/uc0002_5_6_invalid.png (added)
-
P4-Prototype/screenshots/uc0002_7_success.png (added)
-
P4-Prototype/screenshots/uc0002_login.png (deleted)
-
P4-Prototype/screenshots/uc0003_1_2_deposit.png (added)
-
P4-Prototype/screenshots/uc0003_3_6_deposited.png (added)
-
P4-Prototype/screenshots/uc0003_4a_invalid.png (added)
-
P4-Prototype/screenshots/uc0003_deposit.png (deleted)
-
P4-Prototype/screenshots/uc0003_verify_balance.png (added)
-
P4-Prototype/screenshots/uc0004_1_2_markets.png (added)
-
P4-Prototype/screenshots/uc0004_3_4_price.png (added)
-
P4-Prototype/screenshots/uc0004_5_7_executed.png (added)
-
P4-Prototype/screenshots/uc0004_6a_insufficient.png (added)
-
P4-Prototype/screenshots/uc0004_buy.png (deleted)
-
P4-Prototype/screenshots/uc0004_verify_portfolio.png (added)
-
P4-Prototype/screenshots/uc0005_1_2_holdings.png (added)
-
P4-Prototype/screenshots/uc0005_3_4_price.png (added)
-
P4-Prototype/screenshots/uc0005_5_7_executed.png (added)
-
P4-Prototype/screenshots/uc0005_5a_insufficient.png (added)
-
P4-Prototype/screenshots/uc0005_sell.png (deleted)
-
P4-Prototype/screenshots/uc0006_history.png (modified) ( previous)
-
P4-Prototype/screenshots/uc0006_portfolio.png (modified) ( previous)
-
P4-Prototype/screenshots/uc0007_1_3_menu.png (added)
-
P4-Prototype/screenshots/uc0007_add_1_list.png (added)
-
P4-Prototype/screenshots/uc0007_add_2_added.png (added)
-
P4-Prototype/screenshots/uc0007_list.png (added)
-
P4-Prototype/screenshots/uc0007_list_after.png (added)
-
P4-Prototype/screenshots/uc0007_remove_1_list.png (added)
-
P4-Prototype/screenshots/uc0007_remove_2_removed.png (added)
-
P4-Prototype/screenshots/uc0007_remove_invalid.png (added)
-
P4-Prototype/screenshots/uc0007_watchlist.png (deleted)
-
P4-Prototype/wiki/BuildInstructions.md (added)
-
P4-Prototype/wiki/PrototypeImplementation.md (added)
-
P4-Prototype/wiki/PrototypeImplementationAIUsage.md (added)
-
P4-Prototype/wiki/UseCase0001Implementation.md (added)
-
P4-Prototype/wiki/UseCase0002Implementation.md (added)
-
P4-Prototype/wiki/UseCase0003Implementation.md (added)
-
P4-Prototype/wiki/UseCase0004Implementation.md (added)
-
P4-Prototype/wiki/UseCase0005Implementation.md (added)
-
P4-Prototype/wiki/UseCase0006Implementation.md (added)
-
P4-Prototype/wiki/UseCase0007Implementation.md (added)
-
P5-Normalization/Normalization.md (modified) (1 diff)
-
P5-Normalization/wiki/Normalization.md (added)
-
P5-Normalization/wiki/NormalizationAIUsage.md (added)
-
P6-AdvancedReports/wiki/AdvancedReports.md (added)
-
P6-AdvancedReports/wiki/AdvancedReportsAIUsage.md (added)
-
P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md (added)
-
P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopmentAIUsage.md (added)
-
P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopment.md (added)
-
P7-AdvancedDatabaseDevelopment/wiki/AdvancedDatabaseDevelopmentAIUsage.md (added)
-
README.md (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
docs/P1-ConceptualModel/ERModel.md
ra531b45 ref1c1c7 1 # Entity-Relationship Model v.0 31 # Entity-Relationship Model v.04 2 2 3 3 ## Diagram 4 4 5 5  6 6 7 7 Notation: Chen. Rectangles are entity sets, diamonds are relationships, ellipses … … 51 51 | `available_balance` | numeric(18,4) | required, default 0, ≥ 0 | 52 52 | `invested_balance` | numeric(18,4) | required, default 0, ≥ 0 | 53 | `reserved_balance` | numeric(18,4) | required, default 0, ≥ 0 — cash set aside for the user's open buy orders (added in v04, after P7) | 53 54 | `created_at` | timestamptz | required, defaults to now | 54 55 | `updated_at` | timestamptz | optional (null until first change) | … … 96 97 makes the ledger auditable. 97 98 98 Placing an order is what triggers a **reservation** of whatever it commits — 99 the crypto being sold (`Holds.reserved_quantity`, below) on a sell, cash 100 already handled the same way on a buy via `available_balance` / 101 `invested_balance`. `status` therefore has real meaning as a lifecycle, not 102 just a label: `open` means reserved but not yet settled, `executed` means 103 settled, `cancelled` would release the reservation without settling (not yet 104 exercised by any use case, since only market orders — which settle 105 immediately — are implemented). See 99 Placing an order is what triggers a **reservation** of whatever it commits: 100 the crypto being sold (`Holds.reserved_quantity`, below) on a sell, and the 101 cash (`Users.reserved_balance`) on a buy. Since v04 (after P7) an order can 102 wait in the order book and be filled in parts, so `status` is a real 103 lifecycle driven by `filled_quantity`: `open` (nothing filled yet), 104 `partially_filled`, `executed` (completely filled), or `cancelled`, which 105 releases what is still reserved. See 106 106 [UseCase0005](../P3-UseCaseModel/UseCase0005.md) for the reserve-then-settle 107 sequence. 107 sequence and 108 [AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md) 109 for the rules that keep it consistent. 108 110 109 111 **Keys:** candidate `{id}` only — there is no natural key, since the same user … … 115 117 | `id` | UUID | PK, required | 116 118 | `side` | text | required, `buy` or `sell` | 117 | `type` | text | required, `market` or `limit` — the prototype executes only `market`; `limit` exists so the model does not have to change when limit orders are implemented|118 | `status` | text | required, `open`, ` executed` or `cancelled` |119 | `type` | text | required, `market` or `limit` (both executed since P7) | 120 | `status` | text | required, `open`, `partially_filled`, `executed` or `cancelled` | 119 121 | `quantity` | numeric(20,4) | required, > 0 | 120 | `price` | numeric(18,6) | optional — null until the order settles, then the fill price | 122 | `filled_quantity` | numeric(20,4) | required, default 0, between 0 and `quantity` — how much has been traded; remaining = `quantity − filled_quantity` (added in v04, after P7) | 123 | `price` | numeric(18,6) | the limit price; for a market order, the market price when it was placed | 121 124 | `placed_at` | timestamptz | required, defaults to now | 122 125 | `executed_at` | timestamptz | optional, set when the order settles | … … 160 163 | `source` | text(50) | required, default `simulation` — distinguishes a simulated trade from a user's own fill (`user`) | 161 164 165 Since v04 (after P7) a trade also records which orders it filled, through the 166 relationships `FillsBuy` and `FillsSell` below. 167 168 #### OrderEvents 169 *Added in v04, after P7.* The audit trail of an order: one event for its 170 placement, one for every (partial) fill, and one for a cancellation. The 171 `Orders` row only holds the current state; this entity keeps the history of 172 how the order got there. Events are recorded automatically by the database. 173 174 **Keys:** candidate `{id}` only; primary key **`id`** (auto-incrementing 175 integer, events are only read in order). 176 177 | Attribute | Type | Constraints | 178 |---|---|---| 179 | `id` | integer | PK, required, auto-generated | 180 | `event_type` | text | required, `placed`, `partially_filled`, `filled` or `cancelled` | 181 | `quantity` | numeric(20,4) | required — the ordered quantity for `placed`, the filled amount for a fill, the unfilled rest for `cancelled` | 182 | `price` | numeric(18,6) | optional — the order price, or the trade price for a fill | 183 | `status_after` | text | required, the order's status after the event | 184 | `created_at` | timestamptz | required, defaults to now | 185 162 186 #### MarketCandles 163 187 OHLCV aggregates per market and timeframe — the data a price chart is drawn … … 222 246 #### Fills — Markets (1) : MarketTrades (N), total on MarketTrades 223 247 Every executed trade happened on exactly one market. No attributes. 248 249 #### FillsBuy — Orders (1) : MarketTrades (N), partial on both sides 250 *Added in v04, after P7.* The buy order a trade filled. An order can be 251 filled by many trades (partial fills); a trade fills at most one buy order, 252 and none when the simulated market was the buyer. No attributes. 253 254 #### FillsSell — Orders (1) : MarketTrades (N), partial on both sides 255 *Added in v04, after P7.* The sell order a trade filled, symmetric to 256 `FillsBuy`. A trade between two users' orders participates in both. No 257 attributes. 258 259 #### Logs — Orders (1) : OrderEvents (N), total on OrderEvents 260 *Added in v04, after P7.* Every event belongs to exactly one order. No 261 attributes. 224 262 225 263 #### Aggregates — Markets (1) : MarketCandles (N), total on MarketCandles … … 290 328 [UseCase0005](../P3-UseCaseModel/UseCase0005.md) for how the new attribute 291 329 is enforced. 330 - **v04 — after P7.** Phase 7 (order, balance and trade consistency) needed 331 data the model did not have, so the model was extended to stay in line with 332 the database: 333 - `Users.reserved_balance`: cash reserved by open buy orders; 334 - `Orders.filled_quantity` and the status value `partially_filled`: orders 335 can now be filled in parts; 336 - the relationships `FillsBuy` and `FillsSell` between `Orders` and 337 `MarketTrades`: which orders a trade filled; 338 - the entity set `OrderEvents` with the relationship `Logs`: the 339 automatically recorded history of every order. 340 341 Nothing existing was removed or changed. See 342 [AdvancedDatabaseDevelopment](../P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md). 343 The diagram files are `ERModel_v04.xml` / `ERModel_v04.png`; earlier versions 344 are kept. 292 345 293 346 Reasoning for the AI-assisted part of this phase, and the full interaction log, 294 347 are on [ERModelAIUsage](ERModelAIUsage.md). 295 348 296 > **Student action required.** Open `ERModel_v03.xml` in TerraER, read the297 > whole diagram — not just the new `reserved_quantity` ellipse — and change298 > anything you disagree with, including the compaction. The phase rules299 > require the model to be yours; this is a generated revision to review and300 > take over, not an answer to submit unread. -
docs/P1-ConceptualModel/ERModelAIUsage.md
ra531b45 ref1c1c7 283 283 white `(255,255,255)`, matching `ERModel_v02.png`. 284 284 285 > **Student action required.** Open `ERModel_v03.xml` in TerraER and read it 286 > end to end before submission — see the note at the end of 287 > [ERModel](ERModel.md). Everything else in this session's diff is already 288 > applied to the docs and to `server/`. 285 286 ### Session 4 — 2026-09-24 (Claude Opus 5.5): v04 after P7 287 288 **Prompts (student, verbatim):** 289 > But this order_events is added after Phase 7 right? can we add that too? 290 291 > can you make the ERmodel again with TerraER file to update it? and tell that after P7 we added this 292 293 > Make it with a white background like earier versions 294 295 **Response (AI, summarised):** 296 297 - Explained that the P7 changes must also appear in P1 and P2, since both must describe the 298 current data structure. 299 - Built `ERModel_v04.xml` in TerraER's own file format by taking `ERModel_v03.xml` unchanged 300 and appending the new elements with the same XML structure TerraER uses: 301 - the attribute `reserved_balance` on `Users`; 302 - the attribute `filled_quantity` on `Orders`; 303 - the relationships `FillsBuy` and `FillsSell` (Orders 1 : N MarketTrades, partial); 304 - the entity set `OrderEvents` (key `id`, `event_type`, `quantity`, `price`, 305 `status_after`, `created_at`) with `Logs` (Orders 1 : N OrderEvents, total on 306 OrderEvents). 307 - Rendered `ERModel_v04.png` with TerraER 3.14's own drawing classes (loading the `.xml` 308 exactly as TerraER does and using its image export), on a white background and trimmed like 309 the earlier versions. 310 - Updated [ERModel](ERModel.md) (title v.04, new attribute rows, the `OrderEvents` section, the 311 three relationships, and a v04 history entry stating these were added after P7). 312 313 **What I decided:** to add the P7 structure to the ER model. The new elements are placed 314 automatically, so the layout can be tidied by hand in TerraER. -
docs/P2-RelationalDesign/RelationalDesign.md
ra531b45 ref1c1c7 148 148 2. Right-click the database → **ERD For Database** (or open a blank ERD and drag 149 149 the `project` tables in). 150 3. Arrange the tables to mirror `ERModel_v0 1.png`.150 3. Arrange the tables to mirror `ERModel_v03.png`. 151 151 4. **Download image** → PNG, then convert: 152 152 `convert relational_schema.png relational_schema.jpg` -
docs/P3-UseCaseModel/UseCase0004.md
ra531b45 ref1c1c7 10 10 11 11 1. Trader chooses "Place market BUY order". 12 2. System lists the a vailable marketswith their latest price:12 2. System lists the active markets, numbered, with their latest price: 13 13 14 14 ```sql 15 SELECT m.id, c.symbol, m.quote_currency, COALESCE(lp.price, 0) 15 SELECT m.id, c.id, c.symbol, m.quote_currency, 16 COALESCE(lp.price, 0) AS price 16 17 FROM project.markets m 17 18 JOIN project.crypto c ON c.id = m.crypto_id … … 20 21 ORDER BY c.symbol; 21 22 ``` 22 3. Trader enters a market symbol, e.g. `ETH`. 23 4. System resolves the market and looks up the latest price: 23 3. Trader picks the market by its number in the listed markets, e.g. `2` (BTC). 24 4. System takes the chosen row's market id and crypto id from the list (no lookup by 25 symbol) and looks up the latest price: 24 26 25 27 ```sql 26 SELECT m.id, c.id AS crypto_id, c.symbol, m.quote_currency 27 FROM project.markets m 28 JOIN project.crypto c ON c.id = m.crypto_id 29 WHERE upper(c.symbol) = upper($1) AND m.is_active = true; 30 31 SELECT price FROM project.v_latest_prices WHERE market_id = $2; 28 SELECT price FROM project.v_latest_prices WHERE market_id = $1; 32 29 ``` 33 30 5. Trader enters a quantity. … … 91 88 If `available_balance < notional`, the entire transaction rolls back and system shows "Insufficient funds: need X, have Y." 92 89 93 ### Alternate flow 4a — market not found90 ### Alternate flow 3a — number not in the list 94 91 95 If the entered symbol does not match any active market, system shows "market X not found" and returns to the authenticated menu without opening a transaction.92 If the entered number is not one of the listed market numbers, system shows "Invalid choice, enter a number from 1 to N." and returns to the authenticated menu without opening a transaction. -
docs/P3-UseCaseModel/UseCase0005.md
ra531b45 ref1c1c7 26 26 27 27 1. Trader chooses "Place market SELL order". 28 2. System lists markets (same SQL as UC0004 step 2). 29 3. Trader enters market symbol and quantity. 30 4. System resolves the market and looks up the latest price (same SQL as UC0004 step 4). 28 2. System lists, numbered, the Trader's holdings that still have a quantity free to 29 sell (not reserved by an open sell order), with the quantity held, the free 30 quantity and the latest price: 31 32 ```sql 33 SELECT m.id, c.id, c.symbol, m.quote_currency, 34 h.quantity, h.quantity - h.reserved_quantity AS free, 35 COALESCE(lp.price, 0) AS price 36 FROM project.holdings h 37 JOIN project.crypto c ON c.id = h.crypto_id 38 JOIN project.markets m ON m.crypto_id = c.id AND m.is_active = true 39 LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id 40 WHERE h.user_id = $1 41 AND h.quantity - h.reserved_quantity > 0 42 ORDER BY c.symbol; 43 ``` 44 3. Trader picks the holding by its number in the listed holdings, e.g. `2` (ETH), and 45 enters the quantity. 46 4. System takes the chosen row's market id and crypto id from the list (no lookup by 47 symbol) and looks up the latest price: 48 49 ```sql 50 SELECT price FROM project.v_latest_prices WHERE market_id = $1; 51 ``` 31 52 5. System opens a transaction: 32 53 -
docs/P3-UseCaseModel/UseCase0007.md
ra531b45 ref1c1c7 40 40 ### Add a crypto 41 41 42 The Trader picks the crypto by its number from the listed cryptos that are not on the watchlist yet: 43 42 44 ```sql 43 -- 1. resolve the symbol to a crypto_id 44 SELECT id FROM project.crypto WHERE upper(symbol) = upper($1); 45 -- 1. list, numbered, the cryptos not yet on the watchlist 46 SELECT c.id, c.symbol, c.name 47 FROM project.crypto c 48 WHERE NOT EXISTS (SELECT 1 FROM project.watchlist_items wi 49 WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id) 50 ORDER BY c.symbol; 45 51 46 -- 2. insert the item; do nothing if it's already there52 -- 2. insert the chosen crypto ($2 = its id from the list); do nothing if it's already there 47 53 INSERT INTO project.watchlist_items (watchlist_id, crypto_id) 48 VALUES ($ watchlist_id, $crypto_id)54 VALUES ($1, $2) 49 55 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING; 50 56 ``` … … 52 58 ### Remove a crypto 53 59 60 The Trader picks the crypto by its number from the listed cryptos on the watchlist: 61 54 62 ```sql 63 -- 1. list, numbered, the cryptos on the watchlist 64 SELECT c.id, c.symbol, c.name 65 FROM project.watchlist_items wi 66 JOIN project.crypto c ON c.id = wi.crypto_id 67 WHERE wi.watchlist_id = $1 68 ORDER BY c.symbol; 69 70 -- 2. delete the chosen crypto ($2 = its id from the list) 55 71 DELETE FROM project.watchlist_items 56 WHERE watchlist_id = $1 57 AND crypto_id = ( 58 SELECT id FROM project.crypto 59 WHERE upper(symbol) = upper($2) 60 ); 72 WHERE watchlist_id = $1 AND crypto_id = $2; 61 73 ``` 62 74 63 If the delete affects zero rows, system shows "Not in watchlist."75 If the entered number is not one of the listed numbers, system shows "Invalid choice, enter a number from 1 to N." and nothing is changed. -
docs/P4-Prototype/BuildInstructions.md
ra531b45 ref1c1c7 1 1 # Build Instructions 2 2 3 How to compile, configure, run and test the EduBerza prototype.4 Linked from [PrototypeImplementation](PrototypeImplementation.md).3 This page explains how to compile, configure, run and test the EduBerza prototype. 4 It is linked from [PrototypeImplementation](PrototypeImplementation.md). 5 5 6 6 ## Development environment description … … 8 8 | Tool | Version tested | Needed for | 9 9 |-------------|-----------------------|---------------------------------------------------------------| 10 | Go | 1.26 (1.25+ works) | Building `server/` (the CLI) and `bots/` (the market bot). | 11 | PostgreSQL | 16 | The database. Docker image or the faculty server. | 12 | Docker | any recent | Optional — brings up a local PostgreSQL in one command. | 13 | `psql` | any | Optional — running the SQL scripts by hand. | 14 | Java | 21 (8+ works) | Optional — only to open/edit the ER diagram in TerraER. | 15 | DBeaver | any recent | Optional — only to export `relational_schema.jpg`. | 16 17 Nothing else has to be installed. The only third-party Go dependency 18 (`github.com/lib/pq`, the PostgreSQL driver) is fetched automatically by 19 `go build` from `go.mod`/`go.sum`. 10 | Go | 1.26.0 (`go.mod` asks for 1.25 or newer) | Building `server/` (the CLI) and `bots/` (the market bot). | 11 | PostgreSQL | 16.3 (Docker container) | The database. Either the local Docker container or the faculty server. | 12 | Docker + Docker Compose | any recent | Optional. Starts a local PostgreSQL with one command. | 13 | `psql` | 16 | Optional. Only for running the SQL scripts by hand. | 14 | Java | 21 (8+ works) | Optional. Only to open or edit the ER diagram in TerraER. | 15 | DBeaver | any recent | Optional. Only to export `relational_schema.jpg`. | 16 17 About the PostgreSQL version: `docker-compose.yml` uses the image `postgres` without a version 18 tag. Docker therefore starts whatever version of the official image it has pulled. On the 19 machine where the prototype was tested, that was PostgreSQL 16.3. The only extension the schema 20 needs is `pgcrypto` (`CREATE EXTENSION IF NOT EXISTS pgcrypto`). It ships with PostgreSQL and is 21 included in the official image. 22 23 You do not need to install anything else. The only third-party Go library is the PostgreSQL 24 driver `github.com/lib/pq`. `go build` downloads it automatically, at the version pinned in 25 `go.mod` and `go.sum`. 20 26 21 27 ## Build instructions 22 28 23 All commands runfrom the repository root.29 Run all commands from the repository root. 24 30 25 31 ### 1. Configure the database connection … … 29 35 ``` 30 36 31 The defaults in `.env.example` match the bundled Docker setup. To use the32 faculty database instead, either edit `.env` or pass the values as real 33 environment variables — thosetake precedence over the file:37 The defaults in `.env.example` (`localhost:5433`, user `bp_project`, database `bp_database`) 38 match the bundled Docker setup. To use the faculty database instead, edit `.env`, or pass the 39 values as real environment variables. Real environment variables take precedence over the file: 34 40 35 41 ```sh … … 37 43 ``` 38 44 39 `.env` is deliberately not committed (see `.gitignore`) because it holds a 40 password. 45 `.env` is not committed on purpose (see `.gitignore`), because it holds a password. 41 46 42 47 ### 2. Start PostgreSQL … … 46 51 ``` 47 52 48 Skip this step if you are pointing atthe faculty database.53 Skip this step if you use the faculty database. 49 54 50 55 ### 3. Build … … 60 65 ``` 61 66 62 This runs `server/db/schema_creation.sql` and then `server/db/data_load.sql`. 63 Both are **compiled into the binary** (`go:embed`), so `-init` works regardless 64 of which directory you launch it from. It is destructive and idempotent — it 65 drops and recreates the whole `project` schema, so it is also the reset button 66 if a demo goes wrong. To reload only the data, keeping the schema: 67 68 ```sh 69 ./eduberza -load-data 70 ``` 71 72 The equivalent with `psql`, if you prefer to watch the statements run: 67 This runs `server/db/schema_creation.sql` and then `server/db/data_load.sql`. It logs 68 `Running schema_creation.sql ...`, `Running data_load.sql ...` and `Database initialised.`, 69 then prints: 70 71 ``` 72 Schema initialised. Re-run without -init to start the CLI. 73 ``` 74 75 Both scripts are **compiled into the binary** (`go:embed`), so `-init` works from any 76 directory. It is destructive and can be run again any number of times: it drops and recreates 77 the whole `project` schema, so it also resets everything if a demo goes wrong. To reload only 78 the data and keep the schema: 79 80 ```sh 81 ./eduberza -load-data # prints "Sample data reloaded." 82 ``` 83 84 If you prefer to watch the statements run, the same can be done with `psql`: 73 85 74 86 ```sh … … 85 97 ``` 86 98 87 Seed accounts — all with the password `test123`: 88 89 | Username | Starting state | 90 |-----------|-------------------------------------------------------| 91 | `alice` | 8250.00 USD cash, holds 0.5 ETH — best demo account | 92 | `bob` | 5000.00 USD cash, no positions | 93 | `charlie` | 2500.00 USD cash, no positions | 94 95 ### 6. Optional — run the market simulation bot 96 97 In a second terminal: 98 99 ```sh 100 go run ./bots 101 ``` 102 103 The bot walks the price of every active market, inserts a row into 104 `market_trades` on each tick and upserts the current 1-minute candle. Prices in 105 the CLI change while it runs, because the current price is always read from the 106 most recent trade (`v_latest_prices`), never from a stored column. 107 108 ### 7. Optional — richer data for the P6 reports 109 110 `data_load.sql` only seeds a few minutes of trade history, which is not enough 111 for the [top traders](../P6-AdvancedReports/AdvancedReports.md#top-traders-by-realized-performance) 112 or [market performance](../P6-AdvancedReports/AdvancedReports.md#market-performance-leaderboard) 113 reports (menu `[10]`/`[11]`) to show more than a single period. To see them do 114 something more interesting, load five quarters of synthetic history on top: 99 ### 6. Optional: run the market simulation bot 100 101 In a second terminal, also from the repository root (the bot reads `.env` from the current 102 directory): 103 104 ```sh 105 go run ./bots # add -interval 1s for faster ticks; the default is 3s 106 ``` 107 108 On every tick the bot moves the price of every active market by a small random step, inserts a 109 row into `market_trades` and updates the current 1-minute candle. Prices in the CLI change 110 while it runs, because the current price is always read from the most recent trade 111 (`v_latest_prices`) and never from a stored column. Leave the bot off if you want the exact 112 numbers in the tests below. 113 114 ### 7. Optional: richer data for the P6 reports 115 116 `data_load.sql` seeds only a few minutes of trade history. That is not enough for the 117 [top traders](../P6-AdvancedReports/AdvancedReports.md) and 118 [market performance](../P6-AdvancedReports/AdvancedReports.md) reports (menu `[10]` and `[11]`) 119 to show more than one period. To see more interesting results, load five quarters of synthetic 120 history on top: 115 121 116 122 ```sh … … 119 125 ``` 120 126 121 It is deliberately not part of `-init`/`-load-data` — see the header of122 [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) for why — so123 running it never changes the balances the smoke test below checks.127 This script is not part of `-init` or `-load-data` on purpose. The header of 128 [`reports_demo_data.sql`](../../server/db/reports_demo_data.sql) explains why. Running it never 129 changes the balances that the tests below check. 124 130 125 131 ## Testing instructions 126 132 133 ### How to launch and log in 134 135 Start the prototype with `./eduberza` after steps 1–4. The sample data creates three test 136 users. All of them have the password **`test123`**: 137 138 | Username | Starting state after `-init` | 139 |-----------|------------------------------| 140 | `alice` | 8250.00 USD available (1750.00 invested), holds 0.5 ETH bought at 3500.00. Watchlist "Favorites": BTC, ETH, SOL. Best demo account. | 141 | `bob` | 5000.00 USD available, no crypto. Watchlist "Bobs Picks": BTC, DOGE. | 142 | `charlie` | 2500.00 USD available, no crypto, no watchlist yet. | 143 144 The five sample markets are ADA, BTC, DOGE, ETH and SOL, all quoted in USD. Their starting 145 last prices are 0.45375, 67140, 0.122, 3520 and 166.1. 146 127 147 ### Mini-guide to the application 128 148 129 The CLI has two menus. Before logging in: **Register**, **Login**, 130 **Browse markets**. After logging in: **View balance**, **Deposit virtual 131 funds**, **Browse markets**, **Place market BUY order**, **Place market SELL 132 order**, **View portfolio**, **View transaction history**, **Manage watchlist**, 133 **Logout**, and two [P6](../P6-AdvancedReports/AdvancedReports.md) reports: 134 **Report: top traders** and **Report: market performance**. 135 136 You never have to remember an identifier. Markets are always printed as a 137 numbered list with their current price before you are asked which one you want, 138 and assets are referred to by symbol (`BTC`, `ETH`, …), never by database id. 149 You always answer with the number of a menu option. When you have to choose a market, a 150 holding or a crypto, the prototype prints a numbered list and you type the number from that 151 list. You never type an id or a symbol. A number that is not in the list is refused with 152 `Invalid choice, enter a number from 1 to N.` 153 154 **Menu before login** 155 156 | Option | What it does and how to use it | 157 |--------|--------------------------------| 158 | `[1] Register` | Enter a username, an e-mail (must contain `@`), your full name and a password (at least 6 characters). You get `Account created. You can now log in.`, or `Invalid email.`, `Password must be at least 6 characters.` or `Username or email already taken.` A new account starts with 0 USD. | 159 | `[2] Login` | Enter your username and password. You get `Login successful.` and the second menu. A wrong password and an unknown username both give `Invalid credentials.` | 160 | `[3] Browse markets` | Prints the numbered list of markets with their last price. | 161 | `[0] Exit` | Ends the program. | 162 163 **Menu after login** (headed `--- Logged in as <username> ---`) 164 165 | Option | What it does and how to use it | 166 |--------|--------------------------------| 167 | `[1] View balance` | Shows the available, invested and total USD. | 168 | `[2] Deposit virtual funds` | Enter an amount in USD. It must be a positive number, otherwise you get `Invalid amount.` You get `Deposited 500.0000 USD.` | 169 | `[3] Browse markets` | Same list as before login. | 170 | `[4] Place market BUY order` | Lists all markets, numbered, with their last price. Type the number at `Market #:`. The prototype shows the latest price. Type the quantity. You get `Order executed: buy …` or `Insufficient funds: need …, have …`. | 171 | `[5] Place market SELL order` | Lists only the cryptos you hold, numbered, with columns `Held` and `Free to sell`. Type the number at `Holding #:`, then the quantity. You get `Order executed: sell …` or `Insufficient holding: …`. If you hold nothing, you get `you hold no crypto that is free to sell`. | 172 | `[6] View portfolio` | One row per crypto you hold: quantity, reserved, available, average buy price, current price, value and unrealised P/L. Then your cash, portfolio value and net worth. | 173 | `[7] View transaction history` | Your last 20 ledger entries (deposits, buys, sells), newest first. | 174 | `[8] Manage watchlist` | Opens a submenu: `[1] List items` shows your watchlist with last prices. `[2] Add crypto` lists, numbered, the cryptos not on it yet; type a number. `[3] Remove crypto` lists, numbered, the cryptos on it; type a number. `[0] Back` returns. A user without a watchlist gets one named "Favorites" the first time. | 175 | `[9] Logout` | Back to the first menu. | 176 | `[10] Report: top traders` | P6 report. Enter a start date (inclusive) and an end date (exclusive) as `YYYY-MM-DD`. | 177 | `[11] Report: market performance` | P6 report, with the same two dates. | 178 | `[0] Exit` | Ends the program. | 139 179 140 180 ### End-to-end smoke test 141 181 142 Verified on 2026-09-16 against PostgreSQL 16 with freshly loaded sample data. 143 Expected values are exact. 144 145 1. `./eduberza -init` — prints `Database initialised.` 146 2. `./eduberza`, then `[2] Login` → `alice` / `test123` → `Login successful.` 147 3. `[6] View portfolio` → one row: `ETH 0.5000` reserved 0.0000, available 148 0.5000, at avg 3500.000000, current 3520.000000, value 1760.0000, 149 unrealised P/L `+10.0000`. Cash available 8250.0000, net worth 10010.0000. 150 4. `[4] Place market BUY order` → `BTC` → `0.01` → 182 These values were checked on 2026-09-24 against freshly loaded sample data (PostgreSQL 16.3), 183 with the bot not running. The expected values are exact. 184 185 1. `./eduberza -init` prints `Schema initialised. Re-run without -init to start the CLI.` 186 2. `./eduberza`, then `2` (Login), then `alice` / `test123` gives `Login successful.` 187 3. `6` (View portfolio) shows one row: `ETH`, quantity 0.5000, reserved 0.0000, available 188 0.5000, average buy 3500.000000, current 3520.000000, value 1760.0000, unrealised P/L 189 `+10.0000`. Cash available is 8250.0000 and net worth is 10010.0000. 190 4. `4` (BUY). The market list shows `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`. Type `2` at 191 `Market #:`, then `0.01` at `Quantity:`. The result is 151 192 `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`. 152 5. `[6] View portfolio` → now BTC *and* ETH, total value 2431.4000, cash 153 7578.6000 (= 8250.00 − 671.40), net worth still 10010.0000. 154 6. `[5] Place market SELL order` → `ETH` → `0.5` → 193 5. `6` (View portfolio) now shows BTC *and* ETH, with total value 2431.4000, cash 7578.6000 194 (= 8250.00 − 671.40), and net worth still 10010.0000. 195 6. `5` (SELL). The holdings list shows `1 BTC` (held 0.0100) and `2 ETH` (held 0.5000). Type 196 `2` at `Holding #:`, then `0.5`. The result is 155 197 `Order executed: sell 0.5000 ETH @ 3520.000000 (notional 1760.0000 USD)`. 156 7. `[7] View transaction history` → deposit, buy, buy, sell, newest first. 157 8. `[8] Manage watchlist` → `[1] List items` → alice's `Favorites` contains 158 BTC, ETH, SOL with live prices. 159 9. `[9] Logout`, then `[0] Exit`. 198 7. `7` (View transaction history) lists, newest first: the `sell` (+1760.0000), the `buy` of 199 BTC (−671.4000), then the two rows from the sample data, which have the same timestamp: 200 `deposit` 10000.0000 "Initial virtual deposit" and `buy` −1750.0000 "Market buy 0.5 ETH @ 201 3500.00". 202 8. `8` (Manage watchlist), then `1` (List items), shows alice's watchlist with BTC, ETH and SOL 203 and their last prices. Then `2` (Add crypto) lists `1 ADA` and `2 DOGE`; type `1` and you get 204 `Added ADA.` Then `0` (Back). 205 9. `9` (Logout), then `0` (Exit). 160 206 161 207 ### Testing the failure paths 162 208 163 These matter more than the happy path, because they are what proves the 164 transactions actually roll back: 165 166 - **Insufficient funds:** log in as `charlie` (2500 USD) and try to buy `1` BTC. 167 Expect `Insufficient funds: need 67140.0000, have 2500.0000` and *no* change 168 to any table — no order row, no ledger entry, no holding. 169 - **Insufficient holding:** as `bob` (no positions), try to sell `1` ETH. 170 Expect `Insufficient holding: trying to sell 1.0000, available 0.0000 (of 171 0.0000 held, 0.0000 reserved)`. 172 - **Two sell orders racing for the same crypto:** give `alice` a 2 BTC holding 173 and start two `eduberza` processes at once, each selling `1.5` BTC (together 174 3 BTC, more than she has). Expect exactly one `Order executed`, and the 175 other `Insufficient holding` reading the post-commit quantity — see 176 [UseCase0005Implementation](UseCase0005Implementation.md) for the exact 177 transcript. This is the concurrency guarantee that 178 `holdings.reserved_quantity` and the `SELECT ... FOR UPDATE` lock together 179 provide. 209 These matter more than the happy path, because they prove that the transactions really roll 210 back and that invalid choices are refused: 211 212 - **Insufficient funds:** log in as `charlie` (2500 USD). Choose `4`, market `2` (BTC), 213 quantity `1`. Expect `Insufficient funds: need 67140.0000, have 2500.0000` and *no* change to 214 any table: no order row, no ledger entry, no holding. 215 - **Insufficient holding:** on fresh data (`./eduberza -load-data`), log in as `alice`. Choose 216 `5`; the list shows only `1 ETH` (held 0.5000, free 0.5000). Choose `1`, quantity `5`. Expect 217 `Insufficient holding: trying to sell 5.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved)`. 218 - **Nothing to sell:** as `bob` (no crypto), choose `5`. The holdings list is empty, and you 219 get `you hold no crypto that is free to sell` without being asked for a number. 220 - **Invalid choice from a list:** in any list (for example `8`, then `3` Remove crypto), type a 221 number larger than the list. Expect `Invalid choice, enter a number from 1 to N.` 222 - **Invalid deposit:** choose `2` and enter `-50`. Expect `Invalid amount.` 180 223 - **Duplicate registration:** register with username `alice`. Expect 181 224 `Username or email already taken.` 225 - **Invalid e-mail:** register with an e-mail without `@`. Expect `Invalid email.` 182 226 - **Wrong password:** log in as `alice` with any wrong password. Expect 183 `Invalid credentials.` — and note the same message for an unknown username, so 184 the prototype does not leak which accounts exist. 227 `Invalid credentials.` An unknown username gives the same message, so the prototype does not 228 reveal which accounts exist. 229 230 The concurrency guarantee of the sell path (two processes selling the same crypto at the same 231 moment) cannot be reproduced by typing into two terminals, because each order commits within 232 milliseconds. It is described in [UseCase0005Implementation](UseCase0005Implementation.md). 185 233 186 234 ### For the public presentation 187 235 188 Demo with `alice` (already has a position, so the portfolio screen is not189 empty), and register a brand-new account live to show UC0001. Run the bot in a 190 background terminal so the pricesvisibly move between two portfolio refreshes.236 Demo with `alice`. She already has a position, so the portfolio screen is not empty. Register a 237 brand-new account live to show UC0001. Run the bot in a background terminal so the prices 238 visibly move between two portfolio refreshes. 191 239 192 240 ## Editing the ER diagram 193 241 194 TerraER is a third-party tool and is deliberately **not** committed to this 195 repository. Download the teacher's build from 196 <https://bazi.finki.ukim.mk/resources/Software/> and run it: 197 198 ```sh 199 java -jar TerraER3.11.jar # then File → Open → docs/ERModel_v01.xml 200 ``` 201 202 Save new versions as `ERModel_v02.xml`, `ERModel_v03.xml`, … and export a 203 matching PNG for each. TerraER does not add the extension itself — type 204 `.xml` explicitly or the file will not reopen. 242 TerraER is a third-party tool and is **not** committed to this repository on purpose. Download 243 the teacher's build from <https://bazi.finki.ukim.mk/resources/Software/> and run it: 244 245 ```sh 246 java -jar TerraER3.11.jar # then File → Open → docs/P1-ConceptualModel/ERModel_v03.xml 247 ``` 248 249 The current version is `ERModel_v03.xml`. Save new versions as `ERModel_v04.xml` and so on, 250 and export a matching PNG for each. TerraER does not add the extension itself: type `.xml` 251 yourself, or the file will not reopen. 205 252 206 253 ## Up-to-date source code 207 254 208 The repository is pushed to the FINKI DEVELOP git server ; see the Repositories209 section in EPRMS for the clone URL and credentials.255 The repository is pushed to the FINKI DEVELOP git server. The clone URL and credentials are in 256 the Repositories section in EPRMS. 210 257 211 258 ### About the source code 212 259 213 - All source needed to run the prototype is in this repository: the CLI 214 (`server/`), the market bot (`bots/`), the DDL script and the sample-data 215 script (`server/db/`). 216 - Third-party Go libraries are **not** vendored — `go build` downloads 217 `github.com/lib/pq` using the pinned versions in `go.mod` and `go.sum`. 218 - Third-party executables are **not** committed. `.gitignore` excludes `*.jar`; 219 TerraER is downloaded from the URL above. 220 - No third-party images, styles or frameworks are used, and there are no 221 images in the prototype at all — the interface is text. 260 - All the source needed to run the prototype is in this repository: the CLI (`server/`), the 261 market bot (`bots/`), the DDL script and the sample-data script (`server/db/`). 262 - Third-party Go libraries are **not** vendored. `go build` downloads `github.com/lib/pq` at 263 the versions pinned in `go.mod` and `go.sum`. 264 - Third-party executables are **not** committed. `.gitignore` excludes `*.jar`, and TerraER is 265 downloaded from the URL above. 266 - No third-party images, styles or frameworks are used. The prototype has no images at all; 267 the interface is text. -
docs/P4-Prototype/PrototypeImplementation.md
ra531b45 ref1c1c7 1 = Prototype Implementation = 1 # Prototype Implementation 2 2 3 The prototype is a Go command-line application in 4 [https://github.com/StefanTrsunov/bp/tree/main/server server/] that works against the `project` 5 schema in PostgreSQL. It implements all seven use cases from 6 [https://github.com/StefanTrsunov/bp/blob/main/docs/P3-UseCaseModel/UseCaseModel.md UseCaseModel] 7 – the rubric requires at least three – with every database access shown as real, executed SQL. 8 An auxiliary program in [https://github.com/StefanTrsunov/bp/tree/main/bots bots/] simulates a 9 live market so prices move while the prototype is running. 3 EduBerza's P4 prototype is a Go command-line program (in [`server/`](../../server/)). It works 4 against the `project` schema in PostgreSQL. It implements all seven use cases from 5 [UseCaseModel](../P3-UseCaseModel/UseCaseModel.md); the course asks for at least three. Every 6 database access is real SQL that was executed and tested. A second program, the market bot in 7 [`bots/`](../../bots/), simulates a live market so prices move while the prototype runs. 10 8 11 Build, configure, run and test instructions: 12 [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/BuildInstructions.md BuildInstructions]. 9 ## Implemented use-cases 13 10 14 All pages listed below, together with the screenshots of each run, are kept in the project's 15 GitHub repository, [https://github.com/StefanTrsunov/bp StefanTrsunov/bp], under 16 `docs/P4-Prototype/`. 11 - [UseCase0001Implementation](UseCase0001Implementation.md) — Register new account 12 - [UseCase0002Implementation](UseCase0002Implementation.md) — Log in 13 - [UseCase0003Implementation](UseCase0003Implementation.md) — Deposit virtual funds 14 - [UseCase0004Implementation](UseCase0004Implementation.md) — Place market BUY order 15 - [UseCase0005Implementation](UseCase0005Implementation.md) — Place market SELL order 16 - [UseCase0006Implementation](UseCase0006Implementation.md) — View portfolio and transaction history 17 - [UseCase0007Implementation](UseCase0007Implementation.md) — Manage watchlist 17 18 18 == Implemented use-cases == 19 Each page follows its P3 use case step by step. It adds the exact SQL the Go code runs in that 20 step and a screenshot of the step from a real run against the database. Screenshots are in 21 [`screenshots/`](screenshots/). 19 22 20 ||=Page=||=Use-case=||=Source=|| 21 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0001Implementation.md UseCase0001Implementation]||Register a new account||[https://github.com/StefanTrsunov/bp/blob/main/server/auth.go server/auth.go]|| 22 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0002Implementation.md UseCase0002Implementation]||Log in||[https://github.com/StefanTrsunov/bp/blob/main/server/auth.go server/auth.go]|| 23 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0003Implementation.md UseCase0003Implementation]||Deposit virtual funds||[https://github.com/StefanTrsunov/bp/blob/main/server/account.go server/account.go]|| 24 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0004Implementation.md UseCase0004Implementation]||Place market BUY order||[https://github.com/StefanTrsunov/bp/blob/main/server/trade.go server/trade.go]|| 25 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0005Implementation.md UseCase0005Implementation]||Place market SELL order||[https://github.com/StefanTrsunov/bp/blob/main/server/trade.go server/trade.go]|| 26 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0006Implementation.md UseCase0006Implementation]||View portfolio and history||[https://github.com/StefanTrsunov/bp/blob/main/server/portfolio.go server/portfolio.go]|| 27 ||[https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/UseCase0007Implementation.md UseCase0007Implementation]||Manage watchlist||[https://github.com/StefanTrsunov/bp/blob/main/server/watchlist.go server/watchlist.go]|| 23 - How to build, configure, run and test the prototype: [BuildInstructions](BuildInstructions.md) 24 - AI usage for this phase: [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md) 28 25 29 Each page mirrors its P3 use-case page and adds the actual SQL emitted by the Go code plus a 30 screenshot of the corresponding run against the live database. The screenshots are committed 31 alongside the pages, in 32 [https://github.com/StefanTrsunov/bp/tree/main/docs/P4-Prototype/screenshots docs/P4-Prototype/screenshots/]. 26 ## Technology and architecture 33 27 34 == What the prototype demonstrates about the database design == 28 - **Language:** Go (module `bp_project`, `go 1.25` in [`go.mod`](../../go.mod)). The only 29 third-party library is the PostgreSQL driver `github.com/lib/pq`. 30 - **Database:** PostgreSQL. Every table, view and function is in the `project` schema. The DDL 31 is [`schema_creation.sql`](../../server/db/schema_creation.sql) and the sample data is 32 [`data_load.sql`](../../server/db/data_load.sql). Both scripts are compiled into the binary 33 and run by `./eduberza -init`. 34 - **Interface:** plain text menus on standard input and output. There are no web server, 35 frameworks, images or styles. 36 - **Structure:** one source file per area of the application. 35 37 36 * '''The current price is never stored as a column.''' It is always the price of the most recent 37 row in `market_trades`, read through the `v_latest_prices` view. Both the user's own fills and 38 the bot's simulated trades feed the same table, so there is exactly one definition of "the 39 price". 40 * '''Money movements are transactional.''' Buying touches five tables – `orders`, `users`, 41 `holdings`, `transactions`, `market_trades` – inside one transaction. A failed balance check 42 rolls the whole thing back: after a rejected purchase there is no order row, no ledger entry 43 and no holding. This is verified in the failure-path tests in 44 [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/BuildInstructions.md BuildInstructions]. 45 * '''Constraints do real work.''' `UNIQUE (user_id, crypto_id)` on `holdings` is what makes the 46 `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the weighted-average entry price is 47 recomputed by the database in one statement instead of by a read-modify-write in application 48 code. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` is the same idea 49 applied to the sell path: an inconsistent reservation is impossible at the database level, not 50 just something `trade.go` is careful about. 51 * '''Selling reserves before it removes.''' A sell order locks the holding row, reserves the 52 quantity being sold, then settles by removing it — see 53 [UseCase0005Implementation](UseCase0005Implementation.md). Two sell orders placed at the same 54 instant for more than the available quantity are serialised correctly by `SELECT ... FOR 55 UPDATE`, not just by luck of everything happening in one CLI process; this is demonstrated 56 there with two concurrent processes. 57 * '''No identifiers are ever typed.''' Markets are listed with their prices before any choice is 58 made, and everything else is selected by symbol. 38 | File | Responsibility | Use cases | 39 |------|----------------|-----------| 40 | [`server/main.go`](../../server/main.go) | Flags `-init` / `-load-data`, then starts the menu loop | — | 41 | [`server/cli.go`](../../server/cli.go) | The two menus (before and after login), input reading | all | 42 | [`server/db/db.go`](../../server/db/db.go) | Connection from `.env` / environment variables, embedded SQL scripts | — | 43 | [`server/auth.go`](../../server/auth.go) | Register, log in (SHA-256 password hash) | UC0001, UC0002 | 44 | [`server/account.go`](../../server/account.go) | Balance, deposit, transaction history | UC0003, UC0006 | 45 | [`server/market.go`](../../server/market.go) | Market list, choosing a market or a holding by number, latest price | UC0004, UC0005 | 46 | [`server/trade.go`](../../server/trade.go) | Market buy and sell orders, each in one transaction | UC0004, UC0005 | 47 | [`server/portfolio.go`](../../server/portfolio.go) | Portfolio with current value and unrealised P/L | UC0006 | 48 | [`server/watchlist.go`](../../server/watchlist.go) | List, add and remove watchlist items | UC0007 | 49 | [`bots/main.go`](../../bots/main.go) | Market bot: random-walk price ticks into `market_trades`, 1-minute candles | — | 59 50 60 == Known limitations == 51 ## No identifiers to remember 61 52 62 Deliberately out of scope for a first prototype, and the natural content of the later phases:53 The user never has to type or remember an id, a code or a symbol: 63 54 64 * Only `market` orders execute. `limit` is accepted by the schema (`orders.type`) but the 65 matching logic is not implemented. 66 * Passwords are SHA-256 without a salt. Adequate to demonstrate that the password itself is 67 never stored; not adequate for real use. A proper password hash belongs in P9 (security). 68 * Money is handled as `float64` in Go while the database columns are `numeric`. All arithmetic 69 that must be exact – the weighted average – is done in SQL for that reason, but the Go side 70 would need a decimal type for real use. 71 * There is no connection pooling configuration and no explicit isolation level; both are P8 72 topics. 73 * Reservation only ever lives inside one transaction, because only market orders (which settle 74 immediately) exist. A real limit-order matcher would leave `holdings.reserved_quantity` set 75 and `orders.status = 'open'` between two separate commits, and would need a way to cancel an 76 order to release the reservation — neither is implemented, since nothing in the prototype 77 produces an order that stays open. 55 - Every menu is numbered, and the user answers with the number of an option. 56 - **Buying:** all active markets are listed with their latest price, numbered `1…n`. The user 57 enters the market's number at `Market #:` (`ChooseMarket` in `market.go`). 58 - **Selling:** only the cryptos the user actually holds are listed, each with the quantity held 59 and the quantity still free to sell. The user enters the holding's number at `Holding #:` 60 (`ChooseHolding`). A user who holds nothing free to sell gets 61 `you hold no crypto that is free to sell` and is never asked to choose. 62 - **Watchlist:** *Add* lists the cryptos that are not on the watchlist yet. *Remove* lists the 63 ones that are on it. Both are numbered, and the user enters the number. 64 - A number outside the list is refused with `Invalid choice, enter a number from 1 to N.` and 65 nothing is changed. 78 66 79 == AI usage == 67 The only things the user types are their own data: username, e-mail, full name, password, the 68 amount to deposit, the quantity to buy or sell, and the date range of the two P6 reports. 80 69 81 AI was used in this phase and is logged in full, per the course rule for P1 onward. 70 ## What the prototype demonstrates about the database design 82 71 83 * '''Phase log:''' 84 [https://github.com/StefanTrsunov/bp/blob/main/docs/P4-Prototype/PrototypeImplementationAIUsage.md PrototypeImplementationAIUsage.md] 85 – service used, the bugs found and fixed, the test evidence, and what I decided myself. 86 * '''Full conversation transcript:''' 87 [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md ERModelAIUsage.md] 88 – the same conversation produced the P1–P4 artefacts, so the complete prompt/response log is 89 kept in one place. Direct links: 90 [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-1--2026-04-21 Session 1 – 2026-04-21], 91 [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07 Session 2 – 2026-08-06/07], 92 [https://github.com/StefanTrsunov/bp/blob/main/docs/P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16 Session 3 – 2026-09-16]. 72 - **The current price is never stored as a column.** It is always the price of the most recent 73 row in `market_trades`, read through the `v_latest_prices` view. The user's own fills and the 74 bot's simulated trades go into the same table, so there is only one definition of "the 75 price". 76 - **Money movements are transactional.** A buy touches five tables (`orders`, `users`, 77 `holdings`, `transactions`, `market_trades`) inside one transaction. If the balance check 78 fails, the whole transaction is rolled back: after a rejected purchase there is no order row, 79 no ledger entry and no holding. The failure-path tests in 80 [BuildInstructions](BuildInstructions.md) check this. 81 - **Constraints do real work.** `UNIQUE (user_id, crypto_id)` on `holdings` is what makes the 82 `INSERT … ON CONFLICT DO UPDATE` upsert possible, so the database recomputes the 83 weighted-average entry price in one statement, instead of the application reading, changing 84 and writing the row. `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` does 85 the same for the sell path: the database itself makes an inconsistent reservation 86 impossible, and it does not rely only on `trade.go` being careful. 87 - **Selling reserves before it removes.** A sell order locks the holding row with 88 `SELECT … FOR UPDATE`, reserves the quantity being sold, then settles by removing it (see 89 [UseCase0005Implementation](UseCase0005Implementation.md)). Two sell orders for more than the 90 free quantity, placed at the same moment from two separate processes, are serialised by the 91 row lock. Exactly one of them succeeds. This was tested with two concurrent processes in 92 session 3 (see [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md)). 93 93 94 '''Service:''' Claude Code (Anthropic), https://claude.com/claude-code – Claude subscription, 95 model Claude Opus 4.7 (1M context) in sessions 1–2, Claude Sonnet 5 in session 3. 94 ## Known limitations 96 95 97 '''In short:''' session 1 rewrote the existing Chi/HTTP backend as the CLI prototype covering 98 UC0001–UC0007 and added the market bot. Session 2 was a review pass I asked for, which found and 99 fixed three bugs – a path-resolution bug that made the documented build instructions fail, an 100 infinite loop at end of input, and an error check in the wrong order that misreported database 101 failures as "Insufficient holding" – and replaced the read-modify-write holding update with a 102 single `INSERT … ON CONFLICT DO UPDATE`. Session 3 added `holdings.reserved_quantity` and changed 103 `trade.go`'s sell path to reserve crypto before removing it, closing a gap where two sell orders 104 could be granted the same units; see 105 [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md#session-3--2026-09-16). 96 These were left out on purpose for a first prototype. They belong to the later phases: 97 98 - Only `market` orders execute. The schema accepts `limit` (`orders.type`), but there is no 99 matching logic for it. 100 - Passwords are hashed with SHA-256 and no salt. That shows the password itself is never 101 stored, but it is not good enough for real use. A proper password hash belongs in P9 102 (security). 103 - Money is `float64` in Go, while the database columns are `numeric`. For that reason, all 104 arithmetic that must be exact (the weighted average) is done in SQL. Real use would need a 105 decimal type on the Go side too. 106 - The prototype sets no connection pool and no explicit isolation level. Both are P8 topics. 107 - A reservation only exists inside one transaction, because the prototype only has market 108 orders, and they settle immediately. A real limit-order matcher would leave 109 `holdings.reserved_quantity` set and `orders.status = 'open'` between two separate commits. 110 It would also need a way to cancel an order and release the reservation. Neither is 111 implemented, because nothing in the prototype creates an order that stays open. 112 113 ## History of changes 114 115 The code started as my own Go backend: HTTP handlers, a draft schema, and a `db.go` that 116 recreated the tables on every start. It changed as follows. The AI's share of each change is 117 logged in [PrototypeImplementationAIUsage](PrototypeImplementationAIUsage.md). 118 119 | Date | Change | Origin | 120 |------|--------|--------| 121 | 2026-04-21 | My HTTP backend rewritten as the CLI prototype covering UC0001–UC0007. The schema errors in my draft were corrected. The market bot was added. | My code and decisions (CLI instead of web, drop the frontend, keep a simulator); rewrite by AI (session 1) | 122 | 2026-08-06/07 | Three bugs fixed: path resolution of `.env` and the SQL scripts, an endless loop at end of input, and an error check in the wrong order on the sell path. The holding update became one `INSERT … ON CONFLICT DO UPDATE`. | I asked for a code review; fixes by AI (session 2) | 123 | 2026-09-16 | `holdings.reserved_quantity` added. The sell path now reserves, then settles. Orders go from `open` to `executed`. | The edge case was mine; implementation by AI (session 3) | 124 | 2026-09-24 | Every choice is picked from a numbered list: markets by number, a sell lists only the user's holdings, the watchlist lists the cryptos. All screenshots were retaken, one per step. | I asked for a check against the P4 rules; implementation by AI (session 4) | 125 126 **Service:** Claude Code (Anthropic), Claude subscription. Session 1 used Claude Opus 4.7 (1M 127 context), session 2 Claude Opus 5 (1M context), session 3 Claude Sonnet 5, and session 4 128 Claude Opus 5.5 (1M context). -
docs/P4-Prototype/PrototypeImplementationAIUsage.md
ra531b45 ref1c1c7 3 3 ## Name of AI service/solution that was used 4 4 5 **Claude Code** (Anthropic) 5 **Claude Code** (Anthropic), an AI coding assistant that runs in the terminal and reads and 6 edits the project files. 6 7 7 8 - **URL:** https://claude.com/claude-code 8 - **Type of service/subscription:** Claude subscription, model Claude Opus 4.7 (1M context). 9 - **Type of service/subscription:** Claude subscription (Claude Code CLI). A different model was 10 used in each session: 11 12 | Session | Date | Model | 13 |---------|------|-------| 14 | 1 | 2026-04-21 | Claude Opus 4.7 (1M context) | 15 | 2 | 2026-08-06 / 2026-08-07 | Claude Opus 5 (1M context) | 16 | 3 | 2026-09-16 | Claude Sonnet 5 | 17 | 4 | 2026-09-24 | Claude Opus 5.5 (1M context) | 18 19 The same sessions also worked on other phases. This page covers only what concerns the P4 20 prototype and its documentation. 9 21 10 22 ## Final result … … 12 24 ### Results in details / description 13 25 14 During the 2026-04-21 session the AI: 15 16 - Replaced the broken HTTP + frontend scaffolding (which the student had already decided to drop) with a single-binary CLI prototype in Go, split across `main.go`, `cli.go`, `auth.go`, `account.go`, `market.go`, `trade.go`, `portfolio.go`, `watchlist.go`. 17 - Consolidated the broken `db.go` — which used to drop every table on every startup — into a single `Connect()` plus explicit `-init` and `-load-data` flags. 18 - Moved `go.mod` from `server/` to the project root so the same module graph covers both `server/` and `bots/`. 19 - Fixed `go.mod`: removed the unused MySQL driver, promoted `github.com/lib/pq` to a direct dependency. 20 - Implemented the trade flows as real database transactions with row-level `FOR UPDATE` locks, cost-basis bookkeeping, and a ledger row per operation. 21 - Wrote a 140-line market-simulation bot that inserts trades and upserts 1-minute candles every tick. 22 - Exercised the full prototype end-to-end against a running PostgreSQL on port 5433 and verified a sample buy and portfolio view produced the expected numbers. 26 **My own starting code (before any AI was used).** Before session 1 I had written: 27 28 - a Go backend with HTTP handlers (Chi router); 29 - a draft database schema (`server/db/db.sql`, `server/db/schema.sql`) and a diagram description 30 (`docs/dbdiagram.md`), based on my data model in `ep-diagram.md`; 31 - a `main.go` / `db.go` that connected to PostgreSQL and dropped and recreated every table on 32 each start; 33 - a half-finished frontend, which I deleted myself at the start of session 1 because the 34 prototype does not need it. 35 36 This code is older than the git repository. The first commit (2026-08-07) was made after 37 sessions 1 and 2, so the history in git starts from the AI-improved version. My original files 38 are not in the repository. What they contained, and which errors the AI found in them, is 39 recorded in the session 1 log below and in [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md). 40 41 **Session 1 (2026-04-21).** Starting from that code, the AI: 42 43 - replaced my HTTP backend and the frontend scaffolding with a single-binary CLI prototype in Go, 44 split across `main.go`, `cli.go`, `auth.go`, `account.go`, `market.go`, `trade.go`, 45 `portfolio.go` and `watchlist.go`. This followed my decision to make a CLI and not a web app; 46 - corrected my schema. `crypto_id` had been declared as a foreign key to two tables at once in 47 `holdings`, `orders` and `transactions`, and `market_candles` referenced a `markets` table that 48 did not exist. The corrected schema became `schema_creation.sql`, with the sample data in 49 `data_load.sql`; 50 - replaced my `db.go`, which dropped every table on every start, with one `Connect()` plus 51 explicit `-init` and `-load-data` flags; 52 - moved `go.mod` to the project root, removed an unused MySQL driver, and made 53 `github.com/lib/pq` a direct dependency; 54 - wrote the trade flows as database transactions with `FOR UPDATE` row locks, cost-basis 55 bookkeeping and one ledger row per operation; 56 - wrote the market-simulation bot (`bots/main.go`), after I decided to keep a market simulator 57 in the project. 58 59 **Session 2 (2026-08-06/07).** I asked for a code review. The AI found and fixed three bugs 60 (`.env`/SQL path resolution, an endless loop at end of input, and a wrong error check on the 61 sell path). It also replaced the read-modify-write holding update with one 62 `INSERT … ON CONFLICT DO UPDATE`, removed the committed `TerraER3.11.jar` and a TradingView 63 screenshot we had no licence for, and wrote the first version of the P4 pages. 64 65 **Session 3 (2026-09-16).** From an edge case I described, the AI added 66 `holdings.reserved_quantity`, made the sell path reserve and then settle, and gave 67 `orders.status` a real `open` → `executed` lifecycle. 68 69 **Session 4 (2026-09-24).** I asked whether P4 fulfils the course rules. The AI found that the 70 prototype still asked the user to type a market symbol and crypto symbols, which breaks the rule 71 that the user must never have to remember identifiers or codes. It changed every choice to a 72 numbered list (`market.go`, `trade.go`, `watchlist.go`), retook every screenshot, one per 73 scenario step, and rewrote the P4 pages. 23 74 24 75 ### Test evidence 25 76 77 This is the current prototype (after session 4) on fresh sample data, logged in as `alice`. It 78 is a real run, and the same run is on the screenshots of 79 [UseCase0004Implementation](UseCase0004Implementation.md): 80 26 81 ``` 82 -- Place market buy order -- 83 84 # Symbol Quote Last price 85 ----------------------------------------- 86 1 ADA USD 0.453750 87 2 BTC USD 67140.000000 88 3 DOGE USD 0.122000 89 4 ETH USD 3520.000000 90 5 SOL USD 166.100000 91 Market #: 2 92 Latest price for BTC/USD = 67140.000000 93 Quantity: 0.01 27 94 Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD) 28 95 29 Symbol Quantity Avg buy Current Value Unrealised P/L 30 BTC 0.0100 67140.000000 67140.000000 671.4000 +0.0000 31 ETH 0.5000 3500.000000 3520.000000 1760.0000 +10.0000 32 TOTAL 2431.4000 +10.0000 96 Symbol Quantity Reserved Available Avg buy Current Value Unrealised P/L 97 ------------------------------------------------------------------------------------------------------------------ 98 BTC 0.0100 0.0000 0.0100 67140.000000 67140.000000 671.4000 +0.0000 99 ETH 0.5000 0.0000 0.5000 3500.000000 3520.000000 1760.0000 +10.0000 100 ------------------------------------------------------------------------------------------------------------------ 101 TOTAL 2431.4000 +10.0000 33 102 34 103 Cash available : 7578.6000 USD … … 39 108 ## Summary of AI involvement 40 109 41 | | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 | Session 3 — 2026-09-16 | 42 |---|---|---|---| 43 | **What I brought** | My existing Go backend (Chi HTTP handlers) and a half-finished frontend | The CLI prototype as it stood after session 1 | A design review: the sell path had no way to reserve crypto committed to an order | 44 | **What the AI did** | Rewrote the backend as a CLI covering UC0001–UC0007, wrote the market bot | Reviewed the code, found and fixed three bugs, improved the holding upsert | Added `holdings.reserved_quantity`, changed the sell path to reserve-then-settle, gave `Orders.status` a real lifecycle, tested it live including under real concurrency | 45 | **What I decided** | To delete the frontend, to build a CLI rather than a web app, to keep the market simulator | To ask for a code review pass rather than documentation alone | To keep reserve+settle in one transaction rather than split it across two, since there is no cancel-order use case to recover a stuck reservation | 46 47 The prototype was built in session 1 and worked. What session 2 added was a 48 review pass I asked for specifically because I have to defend this code in 49 person: it turned up a path-resolution bug that made my own documented build 50 instructions fail, an infinite loop at end of input, and an error check in the 51 wrong order that misreported database failures as "Insufficient holding". 110 | | Session 1 — 2026-04-21 | Session 2 — 2026-08-06/07 | Session 3 — 2026-09-16 | Session 4 — 2026-09-24 | 111 |---|---|---|---|---| 112 | **What I brought** | My Go backend (Chi HTTP handlers), draft schema, a half-finished frontend | The CLI prototype as it stood after session 1 | A design review: the sell path had no way to reserve crypto committed to an order | The official P4 instructions and the question whether the prototype and pages meet them | 113 | **What the AI did** | Rewrote the backend as a CLI covering UC0001–UC0007, corrected the schema, wrote the market bot | Reviewed the code, found and fixed three bugs, improved the holding upsert | Added `holdings.reserved_quantity`, reserve-then-settle sell path, `orders.status` lifecycle, tested it including real concurrency | Audited P4, made every choice a numbered list, retook all screenshots, rewrote the P4 pages | 114 | **What I decided** | To delete the frontend, to build a CLI and not a web app, to keep the market simulator | To ask for a code review and not only documentation | To keep reserve and settle in one transaction, because there is no cancel-order use case to free a stuck reservation | To fix the problems found and to have the screenshots taken from real runs | 52 115 53 116 ## Entire AI usage log 54 117 55 See [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md) for the full prompt/response transcript of the 2026-04-21 session — that single conversation produced all of the P1–P4 artefacts. The defining student prompt for P4 was: 56 118 **About this log.** Sessions 3 and 4 are still in my local Claude Code history, so their 119 prompts are quoted **verbatim** below, typos included. The local history of sessions 1 and 2 120 (April and August 2026) is no longer available. For them, the prompts are the excerpts I wrote 121 down at the time, the same ones as on [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md), 122 where they were logged because the same conversation produced P1–P4. The AI's responses are 123 summarised, because several ran to many pages of code and file edits. 124 125 --- 126 127 ### Session 1 — 2026-04-21 (Claude Opus 4.7, 1M context) 128 129 **Prompt (student, excerpt):** 130 > This is my project for my exam Database, go to the docs folder and README.md there you will find all of the documentation, afterwards take a look at the whole project. Delete everything you want add everything you want just make it work and make it fill in or to follow all of the needed instructions. Ask me anything, also tell me what do I need to do 131 132 **Response (AI, summarised):** 133 - Read `docs/opis.md`, `docs/ep-diagram.md`, `docs/dbdiagram.md`, `server/db/db.sql`, 134 `server/db/schema.sql` and all Go sources. 135 - Found errors in my schema: `holdings.crypto_id` declared as a foreign key to both 136 `users(id)` and `crypto(id)` (and the same mistake in `orders` and `transactions`), and 137 `market_candles` referencing a non-existent `markets` table. Also found that `main.go` dropped 138 and recreated all tables on every start, wiping the data. 139 - Asked me to decide on team composition, prototype shape (CLI or HTTP), which columns to keep, 140 which use cases to implement, and the documentation language. 141 142 **Prompt (student, excerpt):** 143 > Read it now, just filled it in, also deleted the frontend we don' need that 144 145 **Response (AI, summarised):** 146 - Re-read `docs/README.md`, which now contained the course rubric, and explained the AI rules 147 per phase and the deliverables of each phase. 148 149 **Prompt (student, excerpt):** 57 150 > do everything that you need to do, and tell me after what do I need to do 58 151 59 which translated into the rewrite of the backend and the writing of this documentation. 60 61 > **Student action required:** append any further prompts here where you asked the AI to debug, extend, or refactor the prototype. 62 63 64 ### Session 2 — 2026-08-06 / 2026-08-07 65 66 Prompts are logged in full in [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-2--2026-08-06--2026-08-07). 67 The student asked the AI to complete the phase documentation and to *"fix some 68 database things or golang things if you think we can do it better"*. What the AI 69 changed in the prototype, and why: 152 **Response (AI, summarised):** 153 - Rewrote the backend as the CLI prototype (UC0001–UC0007), with the corrected 154 `schema_creation.sql`, `data_load.sql`, the new `db.go` with `-init` / `-load-data`, the 155 transactional trade flows and the market bot. The details are under *Results in details* 156 above. 157 - Ran the prototype end to end against PostgreSQL on port 5433 and checked that a sample buy 158 and the portfolio view gave the expected numbers. 159 160 --- 161 162 ### Session 2 — 2026-08-06 / 2026-08-07 (Claude Opus 5, 1M context) 163 164 **Prompt (student, excerpt):** 165 > […] do all of the other Phases till m0. 166 > opis.md It's p0 so I will take care of that. Delete anything that we don't need, 167 > make all of the phases and terra diagram if you can, and delete anything 168 > that we don't need and make a documentation about how to start it. 169 170 **Prompt (student, excerpt, follow-up):** 171 > Also fix some database things or golang things if you think we can do it better, 172 173 **Response (AI, summarised): what changed in the prototype, and why** 70 174 71 175 **Bugs found and fixed** 72 176 73 1. `server/db/db.go` resolved `../.env` and `db/schema_creation.sql` as paths 74 relative to the working directory, so they only worked when the program was 75 started from inside `server/`. Following the documented instructions — build 76 from the repository root and run `./eduberza -init` — failed with 77 `password authentication failed for user "postgres"`, because the `.env` file 78 was never found and the defaults were used. The two SQL scripts are now 79 compiled into the binary with `go:embed`, and `.env` is located by searching 80 the working directory and every parent. Real environment variables now take 81 precedence over the file, which is what allows the prototype to be pointed at 82 the faculty database without editing anything. 83 2. `prompt()` in `server/cli.go` ignored the error from `ReadString`. At end of 84 input — Ctrl-D, or a scripted run — it returned an empty string forever and 85 the menu loop spun printing "Unknown option." without end. It now exits 86 cleanly. 87 3. On the sell path, `trade.go` checked `err == sql.ErrNoRows || held < qty` 88 before checking for other errors, so any scan failure was reported to the user 89 as "Insufficient holding" regardless of the real cause. The error check now 90 comes first. 177 1. `server/db/db.go` resolved `../.env` and `db/schema_creation.sql` relative to the working 178 directory, so they only worked when the program was started from inside `server/`. 179 Following the documented instructions (build from the repository root and run 180 `./eduberza -init`) failed with `password authentication failed for user "postgres"`, 181 because `.env` was never found and the defaults were used. The two SQL scripts are now 182 compiled into the binary with `go:embed`, and `.env` is searched for in the working directory 183 and every parent. Real environment variables now take precedence over the file. 184 2. `prompt()` in `server/cli.go` ignored the error from `ReadString`. At end of input (Ctrl-D, 185 or a scripted run) it returned an empty string forever, and the menu loop kept printing 186 "Unknown option." without end. It now exits cleanly. 187 3. On the sell path, `trade.go` checked `err == sql.ErrNoRows || held < qty` before checking 188 for other errors, so any scan failure was reported as "Insufficient holding". The error 189 check now comes first. 91 190 92 191 **Improvements** 93 192 94 4. The holding upsert was a read-modify-write in Go (`SELECT ... FOR UPDATE`, 95 compute the new weighted average in `float64`, then `INSERT` or `UPDATE`). It 96 is now a single `INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`, so the 97 average is recomputed by PostgreSQL in `numeric` arithmetic and the statement 98 relies on the unique constraint that the relational design already declared. 99 5. `TerraER3.11.jar` was removed from the repository and `.gitignore` now 100 excludes `*.jar`, because P4 requires third-party executables to be 101 downloaded rather than committed. `.env` is excluded too and `.env.example` 102 was added in its place. 103 6. `image.png`, a TradingView screenshot, was deleted — the project has no 104 licence to publish it and P4 requires explicit usage rights. 105 106 **Verification** 107 108 All seven use cases were executed against a live PostgreSQL 16 database. The 109 screenshots on the `UseCaseXXXXImplementation` pages are captures of those runs. 110 The four failure paths were tested, and the rollback behaviour was checked 111 directly in SQL: after a rejected purchase, the affected user has zero rows in 112 `orders`, `transactions` and `holdings`. 113 114 > **Student action required:** read the changed files (`server/db/db.go`, 115 > `server/cli.go`, `server/trade.go`, `server/db/schema_creation.sql`) before the 116 > presentation. You will be asked how the buy transaction works, and the answer 117 > has to be yours. 118 119 ### Session 3 — 2026-09-16 120 121 Prompted by a design review I did myself, logged in full in 122 [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md#session-3--2026-09-16). 123 The report: a user who owns 2 BTC and places a sell order for 0.5 BTC has that 124 crypto immediately removed from `quantity`, but nothing in the model recorded 125 that a *pending* order had already committed part of a position before it 126 settled — `holdings` had `quantity` and `avg_price` only, no equivalent of the 127 `available_balance`/`invested_balance` split already used for cash. 128 129 **Bug fixed** 130 131 `server/trade.go`'s sell path checked `held < qty` directly against 132 `holdings.quantity`. This happened to be safe against concurrent double-sells 133 only because the whole operation — order, holding check, holding update, 134 balance update, ledger, trade — runs inside one transaction with a 135 `SELECT ... FOR UPDATE` lock. It was not safe against the actual scenario 136 described: nothing distinguished "owned" from "owned, but already promised to 137 this order," which matters the moment an order can legitimately sit `open` 138 across more than one transaction — exactly what a limit-order matcher would 139 need, and what `orders.status` already implied was coming. 140 141 **What changed** 142 143 1. `holdings.reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 CHECK 144 (reserved_quantity >= 0 AND reserved_quantity <= quantity)` added to 145 `schema_creation.sql`. 146 2. `v_portfolio` gained `reserved_quantity` and derived `available_quantity`. 147 3. `trade.go`'s sell path now: locks the holding, computes 148 `available := quantity - reserved_quantity`, rejects if `available < qty` 149 (previously: rejects if `quantity < qty`), reserves 150 (`reserved_quantity += qty`), then settles (`quantity -= qty; 151 reserved_quantity -= qty`) — two statements instead of one, kept inside the 152 same transaction rather than split into two commits, which would risk an 153 order stuck `open` with reserved crypto and no cancel command to free it. 154 4. Both `PlaceOrder` branches now insert the order as `status='open'` and 155 `UPDATE ... SET status='executed', executed_at=now()` at the end, instead 156 of inserting `'executed'` directly — `Orders.status` is now a real 157 lifecycle rather than a label written once. 158 5. `portfolio.go` gained `Reserved`/`Available` columns, reading 159 `v_portfolio.reserved_quantity`/`available_quantity`, so the new field is 160 something a Trader can actually see. 161 6. The error message on the sell path changed from 162 `"Insufficient holding: trying to sell X, hold Y"` to 163 `"Insufficient holding: trying to sell X, available Y (of Z held, W 164 reserved)"`, since "how much you hold" is no longer the only number that 165 matters. 166 167 **Test evidence** 168 169 Verified against PostgreSQL 16 (`bp_database`, `localhost:5433`): 193 4. The holding upsert was a read-modify-write in Go. It is now a single 194 `INSERT … ON CONFLICT (user_id, crypto_id) DO UPDATE`, so PostgreSQL recomputes the average 195 in `numeric` arithmetic, relying on the unique constraint of the relational design. 196 5. `TerraER3.11.jar` was removed from the repository and `.gitignore` now excludes `*.jar`, 197 because P4 requires third-party executables to be downloaded, not committed. `.env` is 198 excluded too, and `.env.example` was added in its place. 199 6. `image.png`, a TradingView screenshot, was deleted, because the project has no licence to 200 publish it. 201 202 **Verification.** All seven use cases were run against PostgreSQL 16, and the four failure 203 paths were tested. After a rejected purchase, the affected user had zero rows in `orders`, 204 `transactions` and `holdings`. 205 206 --- 207 208 ### Session 3 — 2026-09-16 (Claude Sonnet 5) 209 210 **Prompt (student, verbatim):** 211 > Soo we have a problem here In this scenario we have an edge case where our functionallity doesn't work: 212 > Suppose the user owns: 213 > 214 > 2 BTC 215 > 216 > and wants to sell: 217 > 218 > 0.5 BTC at market price 219 > 220 > A sensible procedure is: 221 > 222 > 1. User creates an Order 223 > 224 > Orders gets something like: 225 > 226 > id user market side type quantity status 227 > O1 Alice BTC/USD sell market 0.5 open 228 > 229 > At this point, no trade has necessarily happened yet. 230 > 231 > 2. Reserve the crypto 232 > 233 > This is where your current model has a gap. 234 > 235 > You currently have: 236 > 237 > Holds 238 > quantity 239 > avg_price 240 > 241 > but you don't have a concept of reserved/committed crypto. 242 > 243 > For cash, you already explicitly distinguish: 244 > 245 > available_balance 246 > invested_balance 247 > 248 > For crypto, you probably want the equivalent: 249 > 250 > quantity 251 > reserved_quantity 252 > avg_price 253 > 254 > So Alice might go from: 255 > 256 > quantity = 2.0 BTC 257 > reserved_quantity = 0 258 > 259 > to: 260 > 261 > quantity = 2.0 BTC 262 > reserved_quantity = 0.5 BTC 263 > 264 > with: 265 > 266 > available BTC = quantity - reserved_quantity = 1.5 BTC 267 > 268 > The important point is that you generally don't actually remove the 0.5 BTC from her position yet. You reserve it so she cannot submit another sell order for the same BTC. 269 > 270 > Can you please start from Phase 1 and modify everything to make it work like it should, modify the AIUsage.md in every Phase, and can you make the ConceptualMOdel a little bit more put together not that much spread out. 271 272 The other prompts of that day were about the P1 diagram (logged on 273 [ERModelAIUsage](../P1-ConceptualModel/ERModelAIUsage.md)) and about P5 and P6. None of them 274 changed the P4 prototype. 275 276 **Response (AI, summarised)** 277 278 The gap: `trade.go`'s sell path checked `held < qty` directly against `holdings.quantity`. 279 Nothing distinguished "owned" from "owned, but already promised to an order". This was safe only 280 because the whole sell runs in one transaction with a `SELECT … FOR UPDATE` lock. It would stop 281 being safe the moment an order could stay `open` across transactions. 282 283 What changed: 284 285 1. `holdings.reserved_quantity numeric(20,4) NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` 286 was added to `schema_creation.sql`. 287 2. `v_portfolio` gained `reserved_quantity` and the derived `available_quantity`. 288 3. The sell path in `trade.go` now locks the holding, computes 289 `available := quantity - reserved_quantity`, rejects the order if `available < qty`, 290 reserves (`reserved_quantity += qty`), then settles (`quantity -= qty; reserved_quantity -= qty`). 291 All of this stays inside the same transaction. 292 4. Both branches of `PlaceOrder` insert the order as `status='open'` and set 293 `status='executed', executed_at=now()` at the end. 294 5. `portfolio.go` shows new `Reserved` and `Available` columns. 295 6. The sell error message became 296 `"Insufficient holding: trying to sell X, available Y (of Z held, W reserved)"`. 297 298 **Test evidence** (PostgreSQL 16, `bp_database`, `localhost:5433`): 170 299 171 300 ``` … … 192 321 ``` 193 322 194 Full transcripts are on 195 [UseCase0005Implementation](UseCase0005Implementation.md). The rejected-order 196 rollback guarantee from session 2 was re-checked too: after both the 197 insufficient-funds and insufficient-holding failure paths, the affected user 198 still has zero new rows in `orders`. 199 200 **What I decided:** to keep reserve and settle inside a single transaction — 201 splitting them into two, so an order genuinely sits `open` and reserved 202 between two commits, is what a real limit-order matcher will eventually need, 203 but building that now would add a way for an order to get stuck without also 204 building a way to cancel it, which is out of scope for this review. `go build 205 ./...` was run after every change; all seven use cases were re-exercised 206 manually. 323 (At that time the sell path still asked for a symbol, so a user with no holding could reach the 324 check. Since session 4, such a user is never offered anything to sell.) 325 326 **What I decided:** to keep reserve and settle inside a single transaction. Splitting them into 327 two commits, so that an order really sits `open` and reserved in between, is what a real 328 limit-order matcher will need. Building it now would add a way for an order to get stuck without 329 a way to cancel it. 330 331 --- 332 333 ### Session 4 — 2026-09-24 (Claude Opus 5.5, 1M context) 334 335 **Prompt (student, verbatim):** 336 > Does the P4 documentation, fullfill this: 337 > 338 > *[pasted: the complete official text "Instructions on Phase P4: First Application Prototype" 339 > from the course site — the prototype requirements, the rule that the user must never have to 340 > remember identifiers or codes, the PrototypeImplementation / UseCaseXXXXImplementation / 341 > BuildInstructions templates, the source-code rules and the AI Use rules with the 342 > PrototypeImplementationAIUsage template]* 343 > 344 > is this correct? 345 > Or do we need to change anything? 346 347 **Response (AI, summarised):** an audit of the prototype and of the P4 pages against each 348 requirement. The main findings: 349 - The prototype asked the user to type a market symbol (`Market symbol (e.g. BTC):`) and, in the 350 watchlist, a crypto symbol. That breaks the rule that every choice must be picked from a list. 351 - The use-case implementation pages had one screenshot per use case, not one per step, and did 352 not show the SQL at every step. 353 - `PrototypeImplementation.md` was written in Trac markup inside a Markdown file. It did not 354 follow the template, and it said identifiers were never typed, which was not accurate. 355 - `BuildInstructions` had wrong details: the `-init` message, the order of the history, an 356 outdated ER diagram path, an unpinned PostgreSQL version described as 16, and a concurrency 357 test with no steps to prepare it. 358 - This page listed only one model, still had "Student action required" notes, and had no 359 line-by-line log. 360 361 **Prompt (student, verbatim):** 362 > YEs you can start fixing it, and can you make the screenshots yourself? 363 364 **Response (AI, summarised):** 365 - **Code** (`market.go`, `trade.go`, `watchlist.go`): 366 - `ListMarkets` now returns the numbered list it prints. `ChooseMarket` asks for `Market #:` 367 and takes the market by its position in that list. 368 - New `ChooseHolding` for the sell path. It lists only the user's holdings that still have a 369 free quantity, with `Held` and `Free to sell`, and asks for `Holding #:`. 370 - Watchlist *Add* lists, numbered, the cryptos not yet on the watchlist. *Remove* lists the ones 371 on it. 372 - A shared `pickNumber` refuses anything outside `1…n` with 373 `Invalid choice, enter a number from 1 to N.` 374 - The SQL that looked a crypto up by symbol (`WHERE upper(c.symbol) = upper($1)`) is gone. The 375 chosen row's id is used directly. 376 - **Screenshots:** all of them were retaken from real CLI sessions, driven in a pseudo-terminal 377 against freshly initialised sample data. There is one screenshot per scenario step, including 378 the alternate flows (invalid e-mail, duplicate user, wrong password, invalid amount, 379 insufficient funds, insufficient holding, invalid list choice). This gave 30 screenshots, 380 which replace the old 8. 381 - **Documentation:** 382 - Rewrote the seven UseCase000XImplementation pages, with the SQL of each step quoted literally 383 from the code. 384 - Rewrote [PrototypeImplementation](PrototypeImplementation.md) (real Markdown, template 385 order, accurate description of the choices), [BuildInstructions](BuildInstructions.md) (the 386 corrections above, a mini-guide of every menu item, and a smoke test re-run on 2026-09-24) 387 and this page. 388 - Made small matching edits to the P3 use cases UC0004, UC0005 and UC0007, so that the "system 389 lists …, user picks …" steps match the prototype. 390 - Wrote the wiki versions of the P4 pages for the faculty site. 391 392 **What I decided:** to accept the numbered-list change, because it is what the P4 rule asks for. 393 I also decided to have the screenshots made from real runs instead of editing the old ones. -
docs/P4-Prototype/UseCase0001Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0001 Implementation — Register1 # Use-case 0001 Implementation - Register new account 2 2 3 **Initiating actor:** Visitor . **Source file:** `server/auth.go`, function `Register`.3 **Initiating actor:** Visitor 4 4 5 ## Scenario (implemented) 5 **Other actors:** — 6 6 7 1. **User** chooses option `[1] Register` from the anonymous menu. 7 A new person creates an account on EduBerza so that they can later log in as a 8 Trader ([UseCase0002](UseCase0002Implementation.md)). The Visitor enters a 9 username, an e-mail address, a full name and a password. The system validates the 10 input (required fields, an `@` in the e-mail, a password of at least 6 characters), 11 refuses a username or e-mail that is already registered, and stores only a SHA-256 12 hash of the password, never the password itself. A new account starts with a cash 13 balance of 0 USD; money is added later with a deposit 14 ([UseCase0003](UseCase0003Implementation.md)). 8 15 16 Original use-case description (P3): [UseCase0001](../P3-UseCaseModel/UseCase0001.md). 17 Implementation: [`server/auth.go`](../../server/auth.go), function `Register` 18 (the password hash is computed by `hashPassword` in the same file). 9 19 10 2. **System** prompts for username, email, full name and password (`server/auth.go:20-23`). 20 ## Scenario 11 21 12 3. **User** enters the values. 22 1. **Visitor** chooses `[1] Register` in the anonymous menu (types `1`). 23 2. **System** prints `-- Register --` and asks, one prompt after another, for 24 `Username:`, `Email:`, `Full name:` and `Password (min 6 chars):`. 13 25 14 4. **System** validates (empty checks, `@` in email, ≥ 6 chars in password) and then asks the database: 26 The screenshot shows steps 1–2: option `1` is chosen and the first prompt 27 (`Username:`) is waiting for input. 28 29  30 31 3. **Visitor** enters the values: `marko`, `marko@example.com`, `Marko Markovski`, 32 `secret1`. 33 4. **System** validates the input in Go, without accessing the database: 34 - username, e-mail and password must be non-empty, otherwise it prints 35 `Username, email and password are required.` and the scenario ends; 36 - the e-mail must contain `@` (see alternate flow 3a); 37 - the password must be at least 6 characters long, otherwise it prints 38 `Password must be at least 6 characters.` and the scenario ends. 39 5. **System** checks whether the username or the e-mail already exists 40 (`$1` = username, `$2` = e-mail): 15 41 16 42 ```sql 17 SELECT EXISTS( 18 SELECT 1 FROM users WHERE username = $1 OR email = $2 19 ); 43 SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2) 20 44 ``` 21 45 22 23 5. **System** inserts the new row (password hashed in Go, not in SQL, to keep hashing identical between register and login): 46 If the result is `true`, alternate flow 5a applies. 47 6. **System** creates the account (`$1` = username, `$2` = e-mail, `$3` = full name, 48 `$4` = password hash). The hash is computed in Go by `hashPassword` as the 49 hex-encoded SHA-256 of the password — the same value that P3's 50 `encode(digest($4, 'sha256'), 'hex')` would produce in SQL. Hashing on the Go side 51 keeps it identical to the check done at login (SQL as in the code, only the Go 52 source indentation removed): 24 53 25 54 ```sql 26 55 INSERT INTO users (username, email, full_name, password_hash, available_balance) 27 VALUES ($1, $2, $3, $4, 0) ;56 VALUES ($1, $2, $3, $4, 0) 28 57 ``` 29 58 30 6. **System** confirms: `Account created. You can now log in.` 59 7. **System** prints `Account created. You can now log in.` and returns to the 60 anonymous menu; the Visitor can continue with 61 [UseCase0002](UseCase0002Implementation.md). 31 62 32  63 The screenshot shows steps 3–7 of the successful attempt (bottom half): the 64 entered values, the confirmation and the anonymous menu again. The top half is the 65 earlier rejected attempt from alternate flow 3a. 66 67  68 69 All statements are run on the `project` schema: the connection sets 70 `search_path=project,public` (`server/db/db.go`), so `users` means `project.users`. 71 72 ### Alternate flow 3a — invalid e-mail 73 74 In step 3 the Visitor entered `marko.example.com` (no `@`). The check in step 4 75 fails, the system prints `Invalid email.` and no SQL statement is executed. In the 76 prototype the system then shows the anonymous menu again and the Visitor chooses 77 `[1] Register` once more, which returns the scenario to step 2. 78 79  80 81 ### Alternate flow 5a — duplicate username or e-mail 82 83 After `marko` has been created, the Visitor tries to register again with username 84 `marko`, e-mail `other@example.com`, full name `Marko Two`, password `secret2`. The 85 query from step 5 (`$1` = `marko`, `$2` = `other@example.com`) returns `true` 86 because the username is taken, so the system prints 87 `Username or email already taken.`, does not run the `INSERT`, and the scenario 88 ends in the anonymous menu. 89 90  33 91 34 92 ## How to reproduce … … 37 95 ./eduberza -init # optional: reset to a known state 38 96 ./eduberza 39 # choose [1] Register, then enter username / email / full name / password 97 # [1] Register: marko / marko.example.com / Marko Markovski / secret1 -> Invalid email. 98 # [1] Register: marko / marko@example.com / Marko Markovski / secret1 -> Account created. 99 # [1] Register: marko / other@example.com / Marko Two / secret2 -> Username or email already taken. 40 100 ``` 41 101 42 The screenshot above is from an actual run: registering `marko` / 43 `marko@example.com`, followed by the confirmation line. 102 All three screenshots come from one real run of exactly these inputs. -
docs/P4-Prototype/UseCase0002Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0002 Implementation —Log in1 # Use-case 0002 Implementation - Log in 2 2 3 **Initiating actor:** Visitor . **Source file:** `server/auth.go`, function `Login` + `authenticate`.3 **Initiating actor:** Visitor 4 4 5 ## Scenario (implemented) 5 **Other actors:** — 6 6 7 1. **User** chooses option `[2] Login`. 7 A registered user authenticates with a username and password so that the system 8 treats all following actions as actions of that Trader. The system looks the user up 9 by username and compares the stored password hash with the SHA-256 hash of the 10 entered password. An unknown username and a wrong password give the same answer, 11 `Invalid credentials.`, so the system does not reveal which usernames exist. After a 12 successful login the user's id and username are kept in the in-process session and 13 the authenticated (Trader) menu is shown, from which all other Trader use-cases 14 start. 8 15 16 Original use-case description (P3): [UseCase0002](../P3-UseCaseModel/UseCase0002.md). 17 Implementation: [`server/auth.go`](../../server/auth.go), functions `Login` and 18 `authenticate` (password hash by `hashPassword`). 9 19 10 2. **System** prompts for username and password. 20 ## Scenario 11 21 12 3. **User** enters `alice` / `test123`. 22 1. **Visitor** chooses `[2] Login` in the anonymous menu (types `2`). 23 2. **System** prints `-- Login --` and asks for `Username:` and then `Password:`. 13 24 14 4. **System** looks up the user and compares hashes: 25 The screenshot shows steps 1–2: option `2` is chosen and the `Username:` prompt 26 is waiting for input. 27 28  29 30 3. **Visitor** enters the username and the password. (If either is empty, the 31 system prints `Username and password are required.` without accessing the 32 database.) 33 4. **System** looks up the user (`$1` = entered username): 15 34 16 35 ```sql 17 SELECT id, password_hash FROM users WHERE username = $1 ;36 SELECT id, password_hash FROM users WHERE username = $1 18 37 ``` 19 38 20 The Go side (`authenticate` in `server/auth.go`) compares the returned `password_hash` against `sha256hex(entered_password)`. 39 5. If no row is returned (`sql.ErrNoRows` in Go), the **System** responds 40 `Invalid credentials.` and the scenario ends. 41 6. If a row is returned, the **System** compares the returned `password_hash` with 42 `hashPassword(entered password)` (hex-encoded SHA-256, computed in Go). On a 43 mismatch it responds `Invalid credentials.` and the scenario ends. 21 44 22 5. **System** on success stores `{UserID, Username}` in the in-process `Session` and shows the authenticated menu. 45 The screenshot shows this failure path with an existing user and a wrong 46 password: `alice` / `wrongpass`. The query from step 4 finds alice's row, the 47 hash comparison of step 6 fails, and the system prints `Invalid credentials.` and 48 returns to the anonymous menu. (An unknown username — step 5 — prints exactly the 49 same message.) 23 50 24  51  52 53 7. On a match, the **System** stores the returned `id` and the username in the 54 session (`s.UserID`, `s.Username`), prints `Login successful.` and displays the 55 authenticated menu headed `--- Logged in as alice ---`. 56 57 The screenshot shows steps 3–7 of the second, successful attempt with the seed 58 credentials `alice` / `test123` (the first, rejected attempt is still visible at 59 the top of the window). 60 61  62 63 The query runs on the `project` schema (the connection sets 64 `search_path=project,public`), so `users` means `project.users`. 65 66 ### Alternate flow 4a (P3) — lookup combined with the live balance 67 68 P3 describes an optional variant that checks the password in SQL and returns the 69 balances in the same query. The P4 prototype does **not** use it: login always uses 70 the query from step 4 with the hash comparison in Go, and the balances are read 71 separately when the Trader asks for them (`[1] View balance`, see 72 [UseCase0003](UseCase0003Implementation.md)). 25 73 26 74 ## Seed credentials 27 75 28 | Username | Password | Balance | 29 |----------|----------|---------| 30 | `alice` | `test123` | 10000.00 USD | 31 | `bob` | `test123` | 5000.00 USD | 32 | `charlie` | `test123` | 2500.00 USD | 76 State after `./eduberza -init` (`server/db/data_load.sql`): 77 78 | Username | Password | Available balance | Invested balance | Holdings | 79 |-----------|-----------|------------------:|-----------------:|----------| 80 | `alice` | `test123` | 8250.00 USD | 1750.00 USD | 0.5 ETH | 81 | `bob` | `test123` | 5000.00 USD | 0.00 USD | — | 82 | `charlie` | `test123` | 2500.00 USD | 0.00 USD | — | 83 84 ## How to reproduce 85 86 ```sh 87 ./eduberza -init 88 ./eduberza 89 # [2] Login: alice / wrongpass -> Invalid credentials. 90 # [2] Login: alice / test123 -> Login successful. (authenticated menu) 91 ``` 92 93 The screenshots come from one real run of exactly these inputs. -
docs/P4-Prototype/UseCase0003Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0003 Implementation — Deposit1 # Use-case 0003 Implementation - Deposit virtual funds 2 2 3 **Initiating actor:** Trader . **Source file:** `server/account.go`, function `Deposit`.3 **Initiating actor:** Trader 4 4 5 ## Scenario (implemented) 5 **Other actors:** — 6 6 7 1. **User** chooses `[2] Deposit virtual funds`. 8 2. **System** prompts for an amount in USD. 9 3. **User** enters `2500`. 10 4. **System** opens a database transaction and runs: 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 ([UseCase0006](UseCase0006Implementation.md)). 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): [UseCase0003](../P3-UseCaseModel/UseCase0003.md). 17 Implementation: [`server/account.go`](../../server/account.go), function `Deposit`; 18 the verification uses `ShowBalance` from the same file. 19 20 Precondition: the Trader is logged in ([UseCase0002](UseCase0002Implementation.md)); 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  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: 11 44 12 45 ```sql 13 46 BEGIN; 14 UPDATE users 15 SET available_balance = available_balance + $1, 16 updated_at = now() 17 WHERE id = $2; 18 INSERT INTO transactions (user_id, type, amount, currency, description) 19 VALUES ($2, 'deposit', $1, 'USD', 'Virtual deposit'); 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 20 56 COMMIT; 21 57 ``` 22 58 23  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 6. **System** confirms `Deposited 500.0000 USD.` and returns to the authenticated 64 menu. 24 65 25 5. **System** confirms: `Deposited 2500.0000 USD.` 66 The screenshot shows steps 3–6: the entered amount `500`, the confirmation and the 67 authenticated menu again. 68 69  70 71 The statements run on the `project` schema (the connection sets 72 `search_path=project,public`), so `users` and `transactions` mean `project.users` 73 and `project.transactions`. 74 75 ### Alternate flow 4a — invalid amount 76 77 If the amount is not a number, or is zero or negative, the system prints 78 `Invalid amount.`; no transaction is started and nothing is written. In the run the 79 Trader first entered `-50`. In the prototype the system then shows the 80 authenticated menu again and the Trader chooses `[2] Deposit virtual funds` once 81 more, which returns the scenario to step 2. 82 83  26 84 27 85 ## Verification 28 86 29 Right after the deposit, `[1] View balance` runs: 87 Right after the deposit the Trader chooses `[1] View balance` (function 88 `ShowBalance`), which runs (`$1` = user id): 30 89 31 90 ```sql 32 SELECT available_balance, invested_balance FROM users WHERE id = $1 ;91 SELECT available_balance, invested_balance FROM users WHERE id = $1 33 92 ``` 34 93 35 In the screenshot above, alice starts from the seed state (available 8250.00, 36 invested 1750.00) and ends at available **10750.00** — increased by exactly the 37 2500.00 deposited, with `invested_balance` untouched. 94 It prints `Available: 8750.0000 USD`, `Invested : 1750.0000 USD`, 95 `Total : 10500.0000 USD`. The available balance grew from the seed value 8250.00 96 by exactly the 500.00 deposited (the rejected `-50` changed nothing), and 97 `invested_balance` is untouched. 98 99  100 101 ## How to reproduce 102 103 ```sh 104 ./eduberza -init 105 ./eduberza 106 # [2] Login: alice / test123 107 # [2] Deposit virtual funds: -50 -> Invalid amount. 108 # [2] Deposit virtual funds: 500 -> Deposited 500.0000 USD. 109 # [1] View balance -> Available: 8750.0000 USD 110 ``` 111 112 All four screenshots come from one real run of exactly these inputs. -
docs/P4-Prototype/UseCase0004Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0004 Implementation — Buy1 # Use-case 0004 Implementation - Place market BUY order 2 2 3 **Initiating actor:** Trader . **Source file:** `server/trade.go`, function `PlaceOrder(s, "buy")`.3 **Initiating actor:** Trader 4 4 5 ## Scenario (implemented) 5 **Other actors:** Market Simulator (indirect — supplies the current price via `market_trades`). 6 6 7 1. **User** chooses `[4] Place market BUY order`. 8 2. **System** lists markets with their latest price (same SQL as UC0006 — via the `v_latest_prices` view). 7 A logged-in Trader buys a crypto asset at the current market price. The Trader never 8 types a symbol or an identifier: the system lists the active markets with their last 9 price, numbered, and the Trader picks one by its number and then enters only the 10 quantity. The system checks that the Trader has enough available cash for 11 quantity × price and then, in one database transaction, records the order, moves the 12 cash from available to invested, adds the crypto to the Trader's holding (recomputing 13 the weighted-average entry price), writes a ledger entry and a market trade, and marks 14 the order executed. The operation touches five tables (`orders`, `users`, `holdings`, 15 `transactions`, `market_trades`) and either all of it succeeds or all of it is rolled 16 back. 9 17 18 Original use-case description (P3): [UseCase0004](../P3-UseCaseModel/UseCase0004.md). 19 Implementation: [`server/trade.go`](../../server/trade.go), function 20 `PlaceOrder(s, "buy")` (with `upsertHoldingOnBuy` in the same file), which calls 21 `ChooseMarket`, `ListMarkets`, `pickNumber` and `LatestPrice` from 22 [`server/market.go`](../../server/market.go). 10 23 11 3. **User** enters `BTC`. 12 4. **System** resolves the market and fetches the latest price: 24 All statements run on the `project` schema: the connection sets 25 `search_path=project,public` (`server/db/db.go`), so `orders` means `project.orders`. 26 The SQL below is copied from the Go code; only the Go source indentation is removed, 27 a `;` is added after each statement of the transaction, and `--` comments say what 28 each `$n` placeholder is bound to. 29 30 The run shown is user `alice` on the seed data (available 8250.00 USD, holding 31 0.5 ETH bought at 3500), buying 0.01 BTC. 32 33 ## Scenario 34 35 1. **Trader** chooses `[4] Place market BUY order` in the authenticated menu (types `4`). 36 2. **System** prints `-- Place market buy order --` and lists all active markets, 37 numbered, with their last price (`ListMarkets`, called by `ChooseMarket`): 13 38 14 39 ```sql 15 SELECT m.id, c.id, c.symbol, m.quote_currency 40 SELECT m.id, c.id, c.symbol, m.quote_currency, 41 COALESCE(lp.price, 0) AS price 16 42 FROM markets m 17 JOIN crypto cON c.id = m.crypto_id18 WHERE upper(c.symbol) = upper($1) AND m.is_active;19 20 SELECT price FROM v_latest_prices WHERE market_id = $2;43 JOIN crypto c ON c.id = m.crypto_id 44 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id 45 WHERE m.is_active = true 46 ORDER BY c.symbol 21 47 ``` 22 48 23 5. **User** enters quantity `0.01`. 24 6. **System** executes a single database transaction — *all or nothing*: 49 The rows are printed in this order as `1 ADA`, `2 BTC`, `3 DOGE`, `4 ETH`, `5 SOL`; 50 Go keeps each row's market id and crypto id in memory, so the Trader only ever sees 51 and types the list number. The system then asks `Market #:`. 52 53  54 55 3. **Trader** picks the market by its number in the list: `2` (BTC/USD). 56 4. **System** takes the market id and crypto id of row 2 from the list (no further 57 lookup by symbol) and reads the latest price of that market (`LatestPrice`; 58 `$1` = the chosen market's id): 59 60 ```sql 61 SELECT price FROM v_latest_prices WHERE market_id = $1 62 ``` 63 64 It prints `Latest price for BTC/USD = 67140.000000` and asks `Quantity:`. 65 66  67 68 5. **Trader** enters the quantity `0.01`. 69 6. **System** computes in Go notional = quantity × price = 0.01 × 67140 = 671.40 and 70 passes it to SQL as a parameter. It then runs one database transaction; the 71 statements below are in exactly the order `PlaceOrder` executes them for a buy: 25 72 26 73 ```sql 27 74 BEGIN; 28 75 29 -- (a) record the order as 'open' — no trade has happened yet 30 INSERT INTO orders31 (user_id, market_id, side, type, status, quantity, price)32 VALUES33 ($1, $2, 'buy', 'market', 'open', $3, $4)34 RETURNING id;76 -- (a) record the order as 'open' — no trade has happened yet. 77 -- $1 = user id, $2 = market id, $3 = side (the Go variable side = 'buy'), 78 -- $4 = quantity (0.01), $5 = price (67140); the returned id is kept in Go. 79 INSERT INTO orders (user_id, market_id, side, type, status, quantity, price) 80 VALUES ($1, $2, $3, 'market', 'open', $4, $5) 81 RETURNING id; 35 82 36 -- (b) lock and check the user balance 83 -- (b) lock the user's row and read the available cash. $1 = user id. 84 -- Go compares it with the notional; if it is smaller -> alternate flow 6a. 37 85 SELECT available_balance FROM users WHERE id = $1 FOR UPDATE; 38 86 39 -- (c) move cash from available to invested. A buy never reserves crypto 40 -- the way a sell does (see UC0005) — it only ever adds to the 41 -- position, so there is nothing to commit on the holdings side 42 -- before settling. 87 -- (c) move the notional from available to invested cash. 88 -- $1 = notional (671.40), $2 = user id. 43 89 UPDATE users 44 SET available_balance = available_balance - $notional,45 invested_balance = invested_balance + $notional,46 updated_at = now()47 WHERE id = $1;90 SET available_balance = available_balance - $1, 91 invested_balance = invested_balance + $1, 92 updated_at = now() 93 WHERE id = $2; 48 94 49 -- (d) upsert the holding, recomputing the weighted-average entry price50 -- in one statement. Every SET expression sees the pre-update row, so51 -- holdings.quantity belowis still the old quantity.52 -- reserved_quantity is untouched by a buy and defaults to 0.95 -- (d) add the crypto to the holding (upsertHoldingOnBuy), recomputing the 96 -- weighted-average entry price in the database. Every SET expression sees 97 -- the pre-update row, so holdings.quantity is still the old quantity. 98 -- $1 = user id, $2 = crypto id, $3 = quantity (0.01), $4 = price (67140). 53 99 INSERT INTO holdings (user_id, crypto_id, quantity, avg_price, updated_at) 54 VALUES ($1, $c, $3, $4, now())55 ON CONFLICT (user_id, crypto_id) DO UPDATE56 SET avg_price = (holdings.quantity * holdings.avg_price57 + EXCLUDED.quantity * EXCLUDED.avg_price)58 / (holdings.quantity + EXCLUDED.quantity),59 quantity = holdings.quantity + EXCLUDED.quantity,60 updated_at = now();100 VALUES ($1, $2, $3, $4, now()) 101 ON CONFLICT (user_id, crypto_id) DO UPDATE 102 SET avg_price = (holdings.quantity * holdings.avg_price 103 + EXCLUDED.quantity * EXCLUDED.avg_price) 104 / (holdings.quantity + EXCLUDED.quantity), 105 quantity = holdings.quantity + EXCLUDED.quantity, 106 updated_at = now(); 61 107 62 -- (e) ledger entry 63 INSERT INTO transactions 64 (user_id, type, amount, currency, related_order, description) 65 VALUES 66 ($1, 'buy', -$notional, 'USD', $orderId, 'Market buy ...'); 108 -- (e) ledger entry. $1 = user id, $2 = -notional (-671.40), $3 = order id from (a), 109 -- $4 = description built in Go: 'Market buy 0.0100 BTC @ 67140.000000'. 110 INSERT INTO transactions (user_id, type, amount, currency, related_order, description) 111 VALUES ($1, 'buy', $2, 'USD', $3, $4); 67 112 68 -- (f) record the resulting market trade 69 INSERT INTO market_trades 70 (market_id, executed_at, price, quantity, side, source) 71 VALUES 72 ($2, now(), $4, $3, 'buy', 'user'); 113 -- (f) record the resulting market trade. 114 -- $1 = market id, $2 = price, $3 = quantity, $4 = side ('buy'). 115 INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source) 116 VALUES ($1, now(), $2, $3, $4, 'user'); 73 117 74 -- (g) settle the order itself — it has now actually been filled 75 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $ orderId;118 -- (g) settle the order itself — it has now actually been filled. $1 = order id. 119 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1; 76 120 77 121 COMMIT; 78 122 ``` 79 123 80 7. **System** prints: `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)`. 124 A buy never reserves crypto (only a sell does, see 125 [UseCase0005](UseCase0005Implementation.md)), so `holdings.reserved_quantity` is not 126 touched and stays 0. 81 127 82  128 7. **System** confirms 129 `Order executed: buy 0.0100 BTC @ 67140.000000 (notional 671.4000 USD)` and shows 130 the authenticated menu again. 83 131 84 ## Verified run (from actual prototype execution) 132 The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu. 85 133 86 Re-run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`) with freshly loaded seed data: 134  87 135 88 - **Before:** alice.available_balance = 8250.00, portfolio = { ETH: 0.5000, reserved 0.0000 }. 89 - **Command:** `buy 0.01 BTC`. 90 - **After:** alice.available_balance = 7578.60 (= 8250 − 671.40), portfolio = { BTC: 0.0100 @ 67140 (reserved 0.0000), ETH: 0.5000 @ 3500 (reserved 0.0000) }, net worth = 10010.00 USD (the +10 is the ETH unrealised P/L from the price moving from 3500 → 3520). A buy never sets `reserved_quantity`, so it reads 0 on every row here. 136 ### Verification — portfolio after the buy 91 137 92 ## Failure path — insufficient funds 138 Right after the buy the Trader chooses `[6] View portfolio` 139 ([UseCase0006](UseCase0006Implementation.md)). It shows the new holding 140 `BTC 0.0100` with average buy price and current price 67140.000000 (value 671.4000), 141 the unchanged `ETH 0.5000` (average 3500, current 3520, unrealised P/L +10.0000), 142 `Cash available : 7578.6000 USD` (= 8250.00 − 671.40), portfolio value 2431.4000 and 143 net worth 10010.0000 USD. The `Reserved` column is 0.0000 on both rows — a buy never 144 reserves anything. 93 145 94 If `available_balance < notional`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above and the user sees: 146  95 147 96 ``` 97 Insufficient funds: need X, have Y 98 ``` 148 ### Alternate flow 6a — insufficient funds 149 150 User `charlie` (seed data: 2500.00 USD available, no crypto) chooses `[4]`, picks 151 market `2` (BTC, 67140.000000) and enters quantity `1`. Go computes the notional 152 67140.00. In the transaction, statement (a) inserts the `open` order and statement (b) 153 `SELECT available_balance FROM users WHERE id = $1 FOR UPDATE` returns 2500.00, which 154 is less than the notional. `PlaceOrder` prints 155 `Insufficient funds: need 67140.0000, have 2500.0000` and returns without running 156 (c)–(g); the deferred `tx.Rollback()` undoes statement (a), so no order, no ledger 157 entry and no balance change is left behind (after this run charlie has no row in 158 `orders` and still 2500.00 USD available). The authenticated menu is shown again. 159 160  161 162 ### Alternate flow 3a — number not in the list 163 164 If in step 3 the Trader enters something that is not a number from 1 to the number of 165 listed markets, `pickNumber` prints `Invalid choice, enter a number from 1 to 5.`, no 166 further SQL is run and the authenticated menu is shown again (the same check is shown 167 in [UseCase0007](UseCase0007Implementation.md), alternate flow 12a). Likewise, a 168 quantity that is not a positive number in step 5 prints `Invalid quantity.` before any 169 transaction is opened. -
docs/P4-Prototype/UseCase0005Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0005 Implementation — Sell 2 3 **Initiating actor:** Trader. **Source file:** `server/trade.go`, function `PlaceOrder(s, "sell")`. 4 5 ## The bug this closes 6 7 Before this change, `holdings` had `quantity` and `avg_price` only. The sell 8 path checked `held < qty` straight against `quantity`, which cannot tell 9 "owned" apart from "owned, but already committed to another order that has 10 not settled." `holdings.reserved_quantity` fixes that: the crypto being sold 11 is reserved before it is removed from the position, and the check is against 12 `quantity - reserved_quantity`. 13 14 ## Scenario (implemented) 15 16 1. **User** chooses `[5] Place market SELL order`. 17 2. **System** lists markets (same as UC0004 step 2). 18 3. **User** enters market symbol, e.g. `ETH`, then quantity `0.5`. 19 4. **System** opens a transaction and runs: 1 # Use-case 0005 Implementation - Place market SELL order 2 3 **Initiating actor:** Trader 4 5 **Other actors:** Market Simulator (indirect — supplies the current price). 6 7 A logged-in Trader sells part or all of a holding at the current market price. The 8 Trader never types a symbol: the system lists only the cryptos the Trader holds and can 9 still sell (the quantity not already reserved by an open sell order), numbered, with 10 how much is held and how much is free, and the Trader picks one by its number and 11 enters the quantity. In one database transaction the system records the order, 12 reserves the crypto being sold and settles it, credits the proceeds to the Trader's 13 available cash while reducing the invested cash by the cost basis, writes a ledger 14 entry and a market trade, and marks the order executed. Cost basis is preserved, so 15 the realised P/L can be reconstructed from the ledger. 16 17 Original use-case description (P3): [UseCase0005](../P3-UseCaseModel/UseCase0005.md). 18 Implementation: [`server/trade.go`](../../server/trade.go), function 19 `PlaceOrder(s, "sell")`, which calls `ChooseHolding`, `pickNumber` and `LatestPrice` 20 from [`server/market.go`](../../server/market.go). 21 22 All 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 24 Go code; only the Go source indentation is removed, a `;` is added after each 25 statement of the transaction, and `--` comments say what each `$n` placeholder is 26 bound to. 27 28 The run shown is user `alice` right after the buy of 29 [UseCase0004](UseCase0004Implementation.md): 7578.60 USD available, holdings 30 0.01 BTC (bought at 67140) and 0.5 ETH (bought at 3500). She sells 0.2 ETH. 31 32 ## Reserve, then settle 33 34 The crypto being sold is **reserved** (`holdings.reserved_quantity`) before it is 35 removed from the position, and the sell check is against what is truly still free, 36 `quantity - reserved_quantity`, not against the raw `quantity`, which would also count 37 crypto already promised to another order. Because only market orders are implemented, 38 an order settles in the same transaction it is placed in, so reserve and settle are two 39 statements inside one commit; they stay logically distinct so that a future 40 limit-order matcher, where an order would stay `open` until a *later* transaction fills 41 it, needs a second transaction but no schema change. 42 43 ## Scenario 44 45 1. **Trader** chooses `[5] Place market SELL order` in the authenticated menu (types `5`). 46 2. **System** prints `-- Place market sell order --` and lists, numbered, only the 47 cryptos the Trader holds with some quantity still free to sell, with the quantity 48 held, the quantity free to sell and the last price (`ChooseHolding`; 49 `$1` = the logged-in user's id): 50 51 ```sql 52 SELECT m.id, c.id, c.symbol, m.quote_currency, 53 h.quantity, h.quantity - h.reserved_quantity AS free, 54 COALESCE(lp.price, 0) AS price 55 FROM holdings h 56 JOIN crypto c ON c.id = h.crypto_id 57 JOIN markets m ON m.crypto_id = c.id AND m.is_active = true 58 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id 59 WHERE h.user_id = $1 60 AND h.quantity - h.reserved_quantity > 0 61 ORDER BY c.symbol 62 ``` 63 64 For alice it prints `1 BTC USD 0.0100 0.0100 67140.000000` and 65 `2 ETH USD 0.5000 0.5000 3520.000000`, then asks `Holding #:`. Go keeps each row's 66 market id and crypto id in memory; the Trader only types the list number. (If the 67 query returns no row, the system prints `you hold no crypto that is free to sell` 68 and the use-case ends.) 69 70  71 72 3. **Trader** picks the holding by its number in the list: `2` (ETH). 73 4. **System** takes the market id and crypto id of row 2 from the list and reads the 74 latest price of that market (`LatestPrice`; `$1` = the chosen market's id): 75 76 ```sql 77 SELECT price FROM v_latest_prices WHERE market_id = $1 78 ``` 79 80 It prints `Latest price for ETH/USD = 3520.000000` and asks `Quantity:`. 81 82  83 84 5. **Trader** enters the quantity `0.2`. 85 6. **System** computes in Go notional = quantity × price = 0.2 × 3520 = 704.00 and 86 runs one database transaction; the statements are in exactly the order 87 `PlaceOrder` executes them for a sell. After statement (b) Go also computes the 88 cost basis = avg_price × quantity = 3500 × 0.2 = 700.00 from the locked holding row; 89 both values are passed to SQL as parameters. 20 90 21 91 ```sql 22 92 BEGIN; 23 93 24 -- (a) record the order as 'open' — no trade has happened yet 25 INSERT INTO orders 26 (user_id, market_id, side, type, status, quantity, price) 27 VALUES 28 ($1, $2, 'sell', 'market', 'open', $3, $4) 29 RETURNING id; 30 31 -- (b) lock the holding and check what is actually free to sell 94 -- (a) record the order as 'open' — no trade has happened yet. 95 -- $1 = user id, $2 = market id, $3 = side (the Go variable side = 'sell'), 96 -- $4 = quantity (0.2), $5 = price (3520); the returned id is kept in Go. 97 INSERT INTO orders (user_id, market_id, side, type, status, quantity, price) 98 VALUES ($1, $2, $3, 'market', 'open', $4, $5) 99 RETURNING id; 100 101 -- (b) lock the holding row and read what is held, what is already reserved and 102 -- the average entry price. $1 = user id, $2 = crypto id. 103 -- Go computes available = quantity - reserved_quantity (0.5 - 0 = 0.5); 104 -- if there is no row or available < quantity -> alternate flow 5a. 32 105 SELECT quantity, reserved_quantity, avg_price FROM holdings 33 WHERE user_id = $1 AND crypto_id = $c FOR UPDATE; 34 -- available := quantity - reserved_quantity 35 -- abort if missing or available < $qty 36 37 -- (c) reserve: committed to this order, not yet removed from the position 106 WHERE user_id = $1 AND crypto_id = $2 FOR UPDATE; 107 108 -- (c) reserve: committed to this order, not yet removed from the position. 109 -- $1 = quantity (0.2), $2 = user id, $3 = crypto id. 38 110 UPDATE holdings 39 SET reserved_quantity = reserved_quantity + $qty, updated_at = now() 40 WHERE user_id = $1 AND crypto_id = $c; 41 42 -- (d) settle: a market order fills immediately, so release the 43 -- reservation and remove the asset in the same step 111 SET reserved_quantity = reserved_quantity + $1, 112 updated_at = now() 113 WHERE user_id = $2 AND crypto_id = $3; 114 115 -- (d) settle: a market order fills immediately, so release the reservation and 116 -- remove the asset from the position in one step. Same parameters as (c). 44 117 UPDATE holdings 45 SET quantity = quantity - $qty, 46 reserved_quantity = reserved_quantity - $qty, 47 updated_at = now() 48 WHERE user_id = $1 AND crypto_id = $c; 49 118 SET quantity = quantity - $1, 119 reserved_quantity = reserved_quantity - $1, 120 updated_at = now() 121 WHERE user_id = $2 AND crypto_id = $3; 122 123 -- (e) credit the proceeds; reduce invested cash by the cost basis. 124 -- $1 = notional (704.00), $2 = cost basis (700.00), $3 = user id. 50 125 UPDATE users 51 SET available_balance = available_balance + $notional,52 invested_balance = GREATEST(invested_balance - ($avg * $qty), 0),53 updated_at = now()54 WHERE id = $1;55 56 INSERT INTO transactions57 (user_id, type, amount, currency, related_order, description)58 VALUES59 ($1, 'sell', $notional, 'USD', $orderId, 'Market sell ...');60 61 INSERT INTO market_trades62 (market_id, executed_at, price, quantity, side, source)63 VALUES64 ($2, now(), $price, $qty, 'sell', 'user');65 66 -- ( e) settle the order itself — it has now actually been filled67 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $ orderId;126 SET available_balance = available_balance + $1, 127 invested_balance = GREATEST(invested_balance - $2, 0), 128 updated_at = now() 129 WHERE id = $3; 130 131 -- (f) ledger entry. $1 = user id, $2 = notional (704.00), $3 = order id from (a), 132 -- $4 = description built in Go: 'Market sell 0.2000 ETH @ 3520.000000'. 133 INSERT INTO transactions (user_id, type, amount, currency, related_order, description) 134 VALUES ($1, 'sell', $2, 'USD', $3, $4); 135 136 -- (g) record the resulting market trade. 137 -- $1 = market id, $2 = price, $3 = quantity, $4 = side ('sell'). 138 INSERT INTO market_trades (market_id, executed_at, price, quantity, side, source) 139 VALUES ($1, now(), $2, $3, $4, 'user'); 140 141 -- (h) settle the order itself — it has now actually been filled. $1 = order id. 142 UPDATE orders SET status = 'executed', executed_at = now() WHERE id = $1; 68 143 69 144 COMMIT; 70 145 ``` 71 146 72  73 74 5. **System** prints: `Order executed: sell 0.5000 ETH @ 3520.000000 (notional 1760.0000 USD)`. 75 76 ## Failure path — insufficient holding 77 78 If the holding does not exist, or `quantity - reserved_quantity < requested`, the `defer tx.Rollback()` in `server/trade.go` reverts every statement above — including the `open` order, which was never committed — and the user sees: 79 80 ``` 81 Insufficient holding: trying to sell X, available Y (of Z held, W reserved) 82 ``` 83 84 ## Verified run — the exact scenario from the design review 85 86 Run 2026-09-16 against PostgreSQL 16 (`bp_database` on `localhost:5433`). 87 Alice's ETH/BTC holdings were seeded, then her BTC holding was set to exactly 88 the scenario that motivated this fix: 2 BTC owned, nothing reserved. 89 90 ``` 91 $ psql ... -c "SELECT symbol, quantity, reserved_quantity, avg_price 92 FROM holdings h JOIN crypto c ON c.id = h.crypto_id 93 WHERE user_id = '<alice>';" 94 95 symbol | quantity | reserved_quantity | avg_price 96 --------+----------+--------------------+------------- 97 BTC | 2.0000 | 0.0000 | 65000.000000 98 ETH | 0.5000 | 0.0000 | 3500.000000 99 ``` 100 101 **Step 1 — portfolio before the sell** (`[6] View portfolio`): 102 103 ``` 104 Symbol Quantity Reserved Available Avg buy Current Value Unrealised P/L 105 ------------------------------------------------------------------------------------------------------------------ 106 BTC 2.0000 0.0000 2.0000 65000.000000 67140.000000 134280.0000 +4280.0000 107 ETH 0.5000 0.0000 0.5000 3500.000000 3520.000000 1760.0000 +10.0000 108 ------------------------------------------------------------------------------------------------------------------ 109 TOTAL 136040.0000 +4290.0000 110 ``` 111 112 **Step 2 — `[5] Place market SELL order` → `BTC` → `0.5`:** 113 114 ``` 115 Order executed: sell 0.5000 BTC @ 67140.000000 (notional 33570.0000 USD) 116 ``` 117 118 **Step 3 — portfolio after the sell:** 119 120 ``` 121 BTC 1.5000 0.0000 1.5000 65000.000000 67140.000000 100710.0000 +3210.0000 122 ``` 123 124 `quantity` dropped from 2.0 to 1.5 and `reserved_quantity` is back to 0.0000 125 — reserve and settle both happened, inside the one commit, exactly as 126 designed. 127 128 ## Verified run — reserve and settle as two distinct, observable steps 129 130 The CLI settles a market order in the same transaction it reserves in, so 131 `reserved_quantity` is never visibly nonzero *outside* a transaction. Run by 132 hand in one `psql` session (one transaction, so the session sees its own 133 uncommitted writes) to show the intermediate state that step (c) alone would 134 leave, before step (d) runs: 147 7. **System** confirms 148 `Order executed: sell 0.2000 ETH @ 3520.000000 (notional 704.0000 USD)` and shows 149 the authenticated menu again. 150 151 The screenshot shows steps 5–7: the entered quantity, the confirmation and the menu. 152 153  154 155 After this run the database holds for alice: ETH `quantity` 0.3000 with 156 `reserved_quantity` 0.0000; `available_balance` 8282.60 (= 7578.60 + 704.00) and 157 `invested_balance` 1721.40 (= 2421.40 − 700.00); a `sell` row in `transactions` with 158 amount 704.0000 and description `Market sell 0.2000 ETH @ 3520.000000`; and the order 159 with status `executed`. The realised P/L of this sell is notional − cost basis = 160 704.00 − 700.00 = +4.00 USD. 161 162 ### Alternate flow 5a — insufficient holding 163 164 Right after the sell above, alice chooses `[5]` again. The list from step 2 now shows 165 `2 ETH USD 0.3000 0.3000 3520.000000`. She picks `2` (ETH) and enters quantity `5`. 166 In the transaction, statement (a) inserts the `open` order and statement (b) returns 167 quantity 0.3000 and reserved_quantity 0.0000, so available = 0.3 < 5. `PlaceOrder` 168 prints 169 170 ``` 171 Insufficient holding: trying to sell 5.0000, available 0.3000 (of 0.3000 held, 0.0000 reserved) 172 ``` 173 174 and returns without running (c)–(h); the deferred `tx.Rollback()` undoes statement (a) 175 as well, so no order, no reservation and no ledger entry is left behind. The 176 authenticated menu is shown again. The same message is printed if the holding row no 177 longer exists (for example because it was sold out from another session after the 178 list was shown). 179 180  181 182 ## Reserve and settle, step by step 183 184 The CLI reserves and settles inside one transaction, so `reserved_quantity` is never 185 nonzero *outside* a transaction. The intermediate state is shown by running statements 186 (c) and (d) by hand in one `psql` transaction (which sees its own uncommitted 187 writes) against alice's ETH holding after the scenario above (0.3 ETH), for a sell of 188 0.1, and rolling back at the end so nothing is changed. Literal values replace the 189 `$n` parameters; `:alice` and `:eth` are psql variables for 190 `(SELECT id FROM users WHERE username = 'alice')` and 191 `(SELECT id FROM crypto WHERE symbol = 'ETH')`: 135 192 136 193 ```sql 137 194 BEGIN; 138 139 -- before: Alice owns 2 BTC, none reserved140 195 SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available 141 FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';196 FROM holdings WHERE user_id = :alice AND crypto_id = :eth; 142 197 -- quantity | reserved_quantity | available 143 -- ----------+--------------------+----------- 144 -- 2.0000 | 0.0000 | 2.0000 145 146 -- step (c): order placed, 0.5 BTC reserved — no trade has happened yet 147 UPDATE holdings SET reserved_quantity = reserved_quantity + 0.5, updated_at = now() 148 WHERE user_id = '<alice>' AND crypto_id = '<btc>'; 149 198 -- ----------+-------------------+----------- 199 -- 0.3000 | 0.0000 | 0.3000 200 201 -- (c) reserve 0.1: the order is placed, no trade has happened yet 202 UPDATE holdings SET reserved_quantity = reserved_quantity + 0.1, updated_at = now() 203 WHERE user_id = :alice AND crypto_id = :eth; 150 204 SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available 151 FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';205 FROM holdings WHERE user_id = :alice AND crypto_id = :eth; 152 206 -- quantity | reserved_quantity | available 153 -- ----------+--------------------+----------- 154 -- 2.0000 | 0.5000 | 1.5000 155 156 -- step (d): market order settles immediately, reservation released 157 UPDATE holdings SET quantity = quantity - 0.5, reserved_quantity = reserved_quantity - 0.5, updated_at = now() 158 WHERE user_id = '<alice>' AND crypto_id = '<btc>'; 159 207 -- ----------+-------------------+----------- 208 -- 0.3000 | 0.1000 | 0.2000 209 210 -- (d) settle: the reservation is released and the asset removed 211 UPDATE holdings SET quantity = quantity - 0.1, reserved_quantity = reserved_quantity - 0.1, updated_at = now() 212 WHERE user_id = :alice AND crypto_id = :eth; 160 213 SELECT quantity, reserved_quantity, quantity - reserved_quantity AS available 161 FROM holdings WHERE user_id = '<alice>' AND crypto_id = '<btc>';214 FROM holdings WHERE user_id = :alice AND crypto_id = :eth; 162 215 -- quantity | reserved_quantity | available 163 -- ----------+--------------------+----------- 164 -- 1.5000 | 0.0000 | 1.5000 165 166 COMMIT; 167 ``` 168 169 This is the row that would stay visible to every other connection for as long 170 as the order stayed `open` — i.e. for as long as it took a matcher to fill 171 it, once limit orders exist. 172 173 ## Verified run — two concurrent sells, which is the bug itself 174 175 The scenario the design review described: a user should not be able to place 176 two sell orders whose combined quantity exceeds what they actually hold. With 177 Alice's BTC holding at 1.5 BTC (0 reserved), two independent CLI processes 178 were started at the same instant, each selling `1.0 BTC` — together 2.0 BTC, 179 more than she has: 180 181 ``` 182 $ ( eduberza-sell-1.0-BTC ) & # process A 183 $ ( eduberza-sell-1.0-BTC ) & # process B 184 $ wait 185 186 === A === 187 Insufficient holding: trying to sell 1.0000, available 0.5000 (of 0.5000 held, 0.0000 reserved) 188 === B === 189 Order executed: sell 1.0000 BTC @ 67140.000000 (notional 67140.0000 USD) 190 191 === final holding === 192 quantity | reserved_quantity 193 ----------+-------------------- 194 0.5000 | 0.0000 195 ``` 196 197 One order settled, one was correctly rejected, and the final `quantity` 198 (0.5) is consistent with exactly one 1.0 BTC sell having happened against the 199 1.5 BTC available — not both, and not neither. This is enforced by the 200 `SELECT ... FOR UPDATE` lock on the holdings row: whichever transaction gets 201 there second blocks until the first commits, then re-reads the now-current 202 `quantity`/`reserved_quantity` before deciding. 203 204 ## Verified — the constraint holds even if application code did not 205 206 ```sql 207 UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = '<alice>' AND crypto_id = '<btc>'; 208 216 -- ----------+-------------------+----------- 217 -- 0.2000 | 0.0000 | 0.2000 218 ROLLBACK; 219 ``` 220 221 The middle state is what every other connection would see for as long as an order 222 stayed `open` once limit orders exist: 0.1 ETH still owned but no longer free to sell. 223 224 ## Two concurrent sells 225 226 A Trader must not be able to sell the same units twice from two sessions at once. Both 227 sessions may have listed the holding as free (step 2 runs outside the transaction), so 228 the protection is statement (b): `SELECT ... FOR UPDATE` locks the holding row, and a 229 second transaction that reaches (b) waits until the first one commits, then reads the 230 already reduced `quantity` before deciding. 231 232 This was checked with two `psql` sessions running statements (b)–(d) against alice's 233 0.3 ETH. Session A locked the row, reserved and settled 0.2 ETH and committed after a 234 3-second pause; session B asked for the lock one second after A had taken it: 235 236 ``` 237 A: SELECT ... FOR UPDATE -> quantity 0.3000, reserved_quantity 0.0000 238 A: reserve 0.2, settle 0.2, pg_sleep(3) 239 B: 11:43:54 SELECT ... FOR UPDATE -- blocks, A holds the row lock 240 A: 11:43:56 COMMIT 241 B: 11:43:56 lock granted -> quantity 0.1000, reserved_quantity 0.0000 242 ``` 243 244 Session B was blocked for the two seconds until A committed and then saw only 245 0.1 ETH, so a second 0.2 ETH sell in B takes alternate flow 5a 246 (`available 0.1000`) instead of selling units that no longer exist. (B was rolled 247 back and alice's holding was restored to 0.3 ETH after the check.) 248 249 ## The constraint holds even if the application code did not 250 251 `schema_creation.sql` declares 252 `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` on 253 `holdings.reserved_quantity`, so an inconsistent reservation is impossible at the 254 database level, independently of `trade.go` (run inside a transaction that was 255 rolled back): 256 257 ``` 258 UPDATE holdings SET reserved_quantity = quantity + 1 WHERE user_id = :alice AND crypto_id = :eth; 209 259 ERROR: new row for relation "holdings" violates check constraint "holdings_check" 210 260 ``` 211 212 `CHECK (reserved_quantity >= 0 AND reserved_quantity <= quantity)` in213 `schema_creation.sql` makes an inconsistent reservation impossible at the214 database level, independent of `trade.go`. -
docs/P4-Prototype/UseCase0006Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0006 Implementation — Portfolio & transactions1 # Use-case 0006 Implementation - View portfolio and transaction history 2 2 3 **Initiating actor:** Trader . **Source files:** `server/portfolio.go` (`ShowPortfolio`), `server/account.go` (`ShowTransactions`, `ShowBalance`).3 **Initiating actor:** Trader 4 4 5 ## Scenario (implemented) 5 **Other actors:** — 6 7 A logged-in Trader inspects the current state of their account. The portfolio view 8 lists every cryptocurrency the Trader holds with the quantity (also split into the 9 part reserved by open sell orders and the part that is free to sell), the average 10 buy price, the current market price, the market value and the unrealised 11 profit/loss, followed by a totals row and a cash summary (cash available, portfolio 12 value, net worth). The transaction history lists the Trader's last 20 ledger 13 entries — deposits, buys and sells — newest first. Both are read-only: nothing in 14 the database is changed. 15 16 Original use-case description (P3): [UseCase0006](../P3-UseCaseModel/UseCase0006.md). 17 Implementation: [`server/portfolio.go`](../../server/portfolio.go), function 18 `ShowPortfolio`, and [`server/account.go`](../../server/account.go), function 19 `ShowTransactions`. 20 21 Precondition: the Trader is logged in ([UseCase0002](UseCase0002Implementation.md)). 22 The run below is alice's, after she bought 0.01 BTC 23 ([UseCase0004](UseCase0004Implementation.md)) and sold 0.2 ETH 24 ([UseCase0005](UseCase0005Implementation.md)) on top of the seed data (0.5 ETH, 25 8250.00 USD cash). 26 27 ## Scenario 6 28 7 29 ### Portfolio 8 30 9 1. **User** chooses `[6] View portfolio`. 10 2. **System** runs: 31 1. **Trader** chooses `[6] View portfolio` in the authenticated menu (types `6`). 32 The menu is the one shown in [UseCase0002](UseCase0002Implementation.md), step 7. 33 2. **System** queries the `v_portfolio` view (`$1` = user id): 11 34 12 35 ```sql 13 SELECT symbol, quantity, 14 COALESCE(reserved_quantity, 0), 36 SELECT symbol, 37 quantity, 38 COALESCE(reserved_quantity, 0), 15 39 COALESCE(available_quantity, quantity), 16 COALESCE(avg_price, 0),17 COALESCE(current_price, 0),18 COALESCE(market_value, 0),40 COALESCE(avg_price, 0), 41 COALESCE(current_price, 0), 42 COALESCE(market_value, 0), 19 43 COALESCE(unrealized_pnl, 0) 20 44 FROM v_portfolio 21 45 WHERE user_id = $1 AND quantity > 0 22 ORDER BY symbol ;46 ORDER BY symbol 23 47 ``` 24 3. **System** renders a table with a totals row, then prints the cash summary: 48 49 3. **System** displays the rows and a `TOTAL` row (sums of the value and P/L columns, 50 computed in Go), then reads the cash balance for the summary (`$1` = user id): 25 51 26 52 ```sql 27 SELECT available_balance, invested_balance FROM users WHERE id = $1 ;53 SELECT available_balance, invested_balance FROM users WHERE id = $1 28 54 ``` 29 55 30  56 and prints `Cash available` (= `available_balance`), `Portfolio value` 57 (= the total market value) and `Net worth` (= their sum). 31 58 32 ### Verified run 59 The screenshot shows the result of steps 2–3. The table is wider than the 60 terminal window, so each long line wraps and the header row has scrolled out of 61 the top of the window; the complete output of this run is reproduced below it. 33 62 34 Re-run 2026-09-16 against PostgreSQL 16 with the seed data (`data_load.sql`). 35 Immediately after login, alice's portfolio prints (now with the 36 `Reserved`/`Available` columns from `holdings.reserved_quantity`): 63  37 64 38 ``` 39 Symbol Quantity Reserved Available Avg buy Current Value Unrealised P/L 40 ------------------------------------------------------------------------------------------------------------------ 41 ETH 0.5000 0.0000 0.5000 3500.000000 3520.000000 1760.0000 +10.0000 42 ------------------------------------------------------------------------------------------------------------------ 43 TOTAL 1760.0000 +10.0000 65 ``` 66 Symbol Quantity Reserved Available Avg buy Current Value Unrealised P/L 67 ------------------------------------------------------------------------------------------------------------------ 68 BTC 0.0100 0.0000 0.0100 67140.000000 67140.000000 671.4000 +0.0000 69 ETH 0.3000 0.0000 0.3000 3500.000000 3520.000000 1056.0000 +6.0000 70 ------------------------------------------------------------------------------------------------------------------ 71 TOTAL 1727.4000 +6.0000 44 72 45 Cash available : 8250.0000 USD46 Portfolio value: 1760.0000 USD47 Net worth : 10010.0000 USD48 ```73 Cash available : 8282.6000 USD 74 Portfolio value: 1727.4000 USD 75 Net worth : 10010.0000 USD 76 ``` 49 77 50 `Reserved` is 0.0000 here because nothing is mid-sell; see 51 [UseCase0005Implementation](UseCase0005Implementation.md) for a portfolio 52 snapshot taken with crypto actually reserved. 78 Checking the figures: ETH is 0.5 − 0.2 = 0.3 at an average buy price of 3500 and 79 a current price of 3520, so value 1056.00 and P/L 0.3 × 20 = +6.00; BTC was just 80 bought at the current price 67140, so its P/L is 0. Cash is 81 8250.00 − 671.40 (buy) + 704.00 (sell) = 8282.60. `Reserved` is 0.0000 for both 82 because the market orders executed immediately — a quantity is reserved only 83 while a sell order is still open. 53 84 54 85 ### Transaction history 55 86 56 1. **User** chooses `[7] View transaction history`. 57 2. **System** runs: 87 1. **Trader** chooses `[7] View transaction history` in the authenticated menu 88 (types `7`). 89 2. **System** queries the last 20 ledger entries of the Trader (`$1` = user id) and 90 prints them, newest first (the time is shown as the first 19 characters of 91 `created_at`): 58 92 59 93 ```sql … … 62 96 WHERE user_id = $1 63 97 ORDER BY created_at DESC 64 LIMIT 20 ;98 LIMIT 20 65 99 ``` 66 100 67  101 The screenshot shows steps 1–2: the choice `7` and the four ledger rows of this 102 run — the sell of 0.2 ETH (+704.0000 USD), the buy of 0.01 BTC (−671.4000 USD), 103 and the two seed rows, the initial deposit of 10000.0000 USD and the seed buy of 104 0.5 ETH (−1750.0000 USD). Buys are stored with a negative amount, deposits and 105 sells with a positive one. The two seed rows were inserted by `data_load.sql` in 106 one statement and have the same `created_at`, so their relative order is not 107 determined by the `ORDER BY`. 108 109  110 111 All statements run on the `project` schema (the connection sets 112 `search_path=project,public`). 113 114 ### Reference — how `v_portfolio` is defined 115 116 From `server/db/schema_creation.sql`: 117 118 ```sql 119 CREATE OR REPLACE VIEW project.v_portfolio AS 120 SELECT h.user_id, 121 c.symbol, 122 h.quantity, 123 h.reserved_quantity, 124 (h.quantity - h.reserved_quantity) AS available_quantity, 125 h.avg_price, 126 lp.price AS current_price, 127 (h.quantity * lp.price) AS market_value, 128 (h.quantity * (lp.price - h.avg_price)) AS unrealized_pnl 129 FROM project.holdings h 130 JOIN project.crypto c ON c.id = h.crypto_id 131 LEFT JOIN project.markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD' 132 LEFT JOIN project.v_latest_prices lp ON lp.market_id = m.id; 133 ``` 134 135 ## How to reproduce 136 137 ```sh 138 ./eduberza -init 139 ./eduberza 140 # [2] Login: alice / test123 141 # [4] buy 0.01 BTC, [5] sell 0.2 ETH (UseCase0004 / UseCase0005) 142 # [6] View portfolio 143 # [7] View transaction history 144 ``` 145 146 Both screenshots come from one real run (portfolio and history taken right after 147 the buy and sell runs). -
docs/P4-Prototype/UseCase0007Implementation.md
ra531b45 ref1c1c7 1 # Use-case 0007 Implementation — Watchlist1 # Use-case 0007 Implementation - Manage watchlist 2 2 3 **Initiating actor:** Trader . **Source file:** `server/watchlist.go`.3 **Initiating actor:** Trader 4 4 5 ## Scenario (implemented) 5 **Other actors:** — 6 6 7 1. **User** chooses `[8] Manage watchlist`. 8 2. **System** ensures a default watchlist exists: 7 A logged-in Trader keeps a list of crypto assets they want to monitor, with the last 8 price of each. The first time the watchlist is opened the system creates a default 9 watchlist named "Favorites" for the Trader. From a sub-menu the Trader can list the 10 watchlist, add a crypto or remove one. The Trader never types a symbol: for adding, the 11 system lists, numbered, only the cryptos that are not on the watchlist yet, and for 12 removing, only the cryptos that are on it; the Trader picks one by its number. Adding 13 a crypto that is already on the list is a no-op (idempotent), and a number that is not 14 in the list is refused without touching the database. 15 16 Original use-case description (P3): [UseCase0007](../P3-UseCaseModel/UseCase0007.md). 17 Implementation: [`server/watchlist.go`](../../server/watchlist.go), functions 18 `ManageWatchlist`, `ensureDefaultWatchlist`, `listWatchlist`, `addToWatchlist` and 19 `removeFromWatchlist`, with `pickNumber` from 20 [`server/market.go`](../../server/market.go). 21 22 All 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 24 Go code; only the Go source indentation is removed. 25 26 The run shown is user `alice` on the seed data, whose watchlist contains BTC, ETH and 27 SOL. She lists it, adds ADA, tries to remove a number that is not in the list, removes 28 SOL 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): 9 35 10 36 ```sql 11 SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1; 12 -- else 13 INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id; 37 SELECT id FROM watchlists WHERE user_id = $1 ORDER BY created_at LIMIT 1 14 38 ``` 15 39 16 3. **System** offers the submenu: List, Add, Remove, Back. 40 Only if this returns no row, it creates the default watchlist and uses its id: 17 41 18 ### List 42 ```sql 43 INSERT INTO watchlists (user_id, name) VALUES ($1, 'Favorites') RETURNING id 44 ``` 19 45 20 ```sql 21 SELECT c.symbol, c.name, COALESCE(lp.price, 0) 22 FROM watchlist_items wi 23 JOIN crypto c ON c.id = wi.crypto_id 24 LEFT JOIN markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD' 25 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id 26 WHERE wi.watchlist_id = $1 27 ORDER BY c.symbol; 28 ``` 46 (alice already has the seed watchlist "Favorites", so only the `SELECT` runs.) The 47 watchlist id is kept in Go and used as `$1` in all statements below. 48 3. **System** shows the sub-menu `-- Watchlist --` with `[1] List items`, 49 `[2] Add crypto`, `[3] Remove crypto` and `[0] Back`. 29 50 30 51  31 52 32 ### Add53 ### List items 33 54 34 ```sql 35 SELECT id FROM crypto WHERE upper(symbol) = upper($1); 55 4. **Trader** chooses `[1] List items`. 56 5. **System** lists the cryptos on the watchlist with their last price against USD 57 (`listWatchlist`; `$1` = watchlist id): 36 58 37 INSERT INTO watchlist_items (watchlist_id, crypto_id) 38 VALUES ($watchlist_id, $crypto_id) 39 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING; 40 ``` 59 ```sql 60 SELECT c.symbol, c.name, COALESCE(lp.price, 0) 61 FROM watchlist_items wi 62 JOIN crypto c ON c.id = wi.crypto_id 63 LEFT JOIN markets m ON m.crypto_id = c.id AND m.quote_currency = 'USD' 64 LEFT JOIN v_latest_prices lp ON lp.market_id = m.id 65 WHERE wi.watchlist_id = $1 66 ORDER BY c.symbol 67 ``` 41 68 42 Re-adding the same symbol is a no-op thanks to the unique constraint + `ON CONFLICT`. 69 For alice it prints `BTC Bitcoin 67140.000000`, `ETH Ethereum 3520.000000` and 70 `SOL Solana 166.100000` (an empty watchlist prints `(watchlist is empty)`), then 71 shows the sub-menu again. 43 72 44 ### Remove 73  45 74 46 ```sql 47 DELETE FROM watchlist_items 48 WHERE watchlist_id = $1 49 AND crypto_id = (SELECT id FROM crypto WHERE upper(symbol) = upper($2)); 50 ``` 75 ### Add a crypto 76 77 6. **Trader** chooses `[2] Add crypto`. 78 7. **System** lists, numbered, the cryptos that are not on the watchlist yet 79 (`addToWatchlist`; `$1` = watchlist id): 80 81 ```sql 82 SELECT c.id, c.symbol, c.name 83 FROM crypto c 84 WHERE NOT EXISTS (SELECT 1 FROM watchlist_items wi 85 WHERE wi.watchlist_id = $1 AND wi.crypto_id = c.id) 86 ORDER BY c.symbol 87 ``` 88 89 For alice it prints `1 ADA Cardano` and `2 DOGE Dogecoin` and asks 90 `Crypto # to add:`. Go keeps each row's crypto id in memory. (If every crypto is 91 already on the watchlist, it prints `Every crypto is already on your watchlist.` 92 instead.) 93 94  95 96 8. **Trader** picks the crypto by its number in the list: `1` (ADA). 97 9. **System** adds the crypto of row 1 to the watchlist (`$1` = watchlist id, 98 `$2` = the chosen crypto's id); thanks to the unique constraint and 99 `ON CONFLICT ... DO NOTHING`, adding a crypto that is already there changes 100 nothing: 101 102 ```sql 103 INSERT INTO watchlist_items (watchlist_id, crypto_id) 104 VALUES ($1, $2) 105 ON CONFLICT (watchlist_id, crypto_id) DO NOTHING 106 ``` 107 108 It prints `Added ADA.` and shows the sub-menu again. 109 110  111 112 ### Remove a crypto 113 114 10. **Trader** chooses `[3] Remove crypto`. 115 11. **System** lists, numbered, the cryptos that are on the watchlist 116 (`removeFromWatchlist`; `$1` = watchlist id): 117 118 ```sql 119 SELECT c.id, c.symbol, c.name 120 FROM watchlist_items wi 121 JOIN crypto c ON c.id = wi.crypto_id 122 WHERE wi.watchlist_id = $1 123 ORDER BY c.symbol 124 ``` 125 126 For alice it now prints `1 ADA Cardano`, `2 BTC Bitcoin`, `3 ETH Ethereum` and 127 `4 SOL Solana` and asks `Crypto # to remove:`. Go keeps each row's crypto id in 128 memory. (If the watchlist is empty, it prints `Your watchlist is empty.` instead.) 129 130  131 132 12. **Trader** picks the crypto by its number in the list: `4` (SOL). 133 13. **System** removes the crypto of row 4 from the watchlist (`$1` = watchlist id, 134 `$2` = the chosen crypto's id): 135 136 ```sql 137 DELETE FROM watchlist_items WHERE watchlist_id = $1 AND crypto_id = $2 138 ``` 139 140 It prints `Removed SOL.` and shows the sub-menu again. 141 142  143 144 #### Alternate flow 12a — number not in the list 145 146 Before removing SOL, alice first chose `[3] Remove crypto` and, at step 12, entered `5` 147 while only numbers 1–4 were listed. `pickNumber` prints 148 `Invalid choice, enter a number from 1 to 4.`, the `DELETE` is not run and the sub-menu 149 is shown again; she then chose `[3]` once more, which returned the scenario to step 11. 150 The same check applies to the number entered at step 8. 151 152  153 154 ### Verification — list after the changes 155 156 Choosing `[1] List items` again runs the query from step 5, which now returns 157 `ADA Cardano 0.453750`, `BTC Bitcoin 67140.000000` and `ETH Ethereum 3520.000000`: 158 ADA was added and SOL removed. `[0] Back` returns to the authenticated menu. 159 160  -
docs/P5-Normalization/Normalization.md
ra531b45 ref1c1c7 257 257 again: nothing was lost. 258 258 259 **Lossless join.** For every pair (referencing relation, referenced relation) connected by a 260 foreign key — `R_MARKETS.M_CRYPTO_ID → R_CRYPTO.C_ID`, `R_HOLDINGS.H_USER_ID → R_USERS.U_ID` / 261 `R_HOLDINGS.H_CRYPTO_ID → R_CRYPTO.C_ID`, `R_ORDERS.O_USER_ID → R_USERS.U_ID` / 262 `R_ORDERS.O_MARKET_ID → R_MARKETS.M_ID`, `R_TRANSACTIONS.T_USER_ID → R_USERS.U_ID` / 263 `R_TRANSACTIONS.T_RELATED_ORDER → R_ORDERS.O_ID`, `R_MARKET_TRADES.MT_MARKET_ID → 264 R_MARKETS.M_ID`, `R_MARKET_CANDLES.MC_MARKET_ID → R_MARKETS.M_ID`, 265 `R_WATCHLISTS.W_USER_ID → R_USERS.U_ID`, `R_WATCHLIST_ITEMS.WI_WATCHLIST_ID → 266 R_WATCHLISTS.W_ID` / `R_WATCHLIST_ITEMS.WI_CRYPTO_ID → R_CRYPTO.C_ID` — the join attribute on 267 the "one" side is that relation's own primary key (`U_ID`, `C_ID`, `M_ID`, `O_ID`, `W_ID`). 268 A join on a foreign key equated to the primary key it references is the textbook sufficient 269 condition for a lossless decomposition (`Ri ∩ Rj` is a key of `Rj`), so re-joining all ten 270 relations on their foreign-key/primary-key pairs reconstructs `R_EDUBERZA` exactly, with no 271 spurious rows and none missing. 259 **Lossless join — chase test.** 260 261 > *Note: the chase algorithm is not part of the course material. I was curious about a 262 > stricter way to test lossless join than the usual "the common attributes are a key of one 263 > side" argument, so I applied it here.* 264 265 The chase decides whether a decomposition `R = R1 ∪ … ∪ Rn` is lossless under a set of 266 functional dependencies. Build a tableau with one column per attribute of `R` and one row per 267 relation `Ri`. In row `i`, put a distinguished symbol `a` in every column of `Ri` and a unique 268 symbol `b_i` in every other column. Then repeat, until nothing changes: for each FD `X → Y`, 269 whenever two rows agree on all of `X`, make them agree on `Y`. If they disagree, an `a` wins, 270 otherwise one `b` replaces the other. **The decomposition is lossless exactly when some row 271 ends up with `a` in every column.** 272 273 All attributes of one cluster (`U_*`, `C_*`, `M_*`, …) always appear together, and FD1–FD17 274 never mix clusters. So each cluster is one column group below: `a` means every column of the 275 group holds a distinguished symbol, and `b` means none of them does. The foreign-key 276 attributes (`H_USER_ID`, `O_MARKET_ID`, …) belong to their own cluster (`H_*`, `O_*`, …), not 277 to the cluster they reference. 278 279 **Step 1 — the ten relations from the table above.** 280 281 ``` 282 U* C* M* H* O* T* MT* MC* W* WI* 283 R_USERS a b b b b b b b b b 284 R_CRYPTO b a b b b b b b b b 285 R_MARKETS b b a b b b b b b b 286 R_HOLDINGS b b b a b b b b b b 287 R_ORDERS b b b b a b b b b b 288 R_TRANSACTIONS b b b b b a b b b b 289 R_MARKET_TR. b b b b b b a b b b 290 R_MARKET_CA. b b b b b b b a b b 291 R_WATCHLISTS b b b b b b b b a b 292 R_WATCHLIST_I. b b b b b b b b b a 293 ``` 294 295 Every FD has its left side inside one cluster, for example `U_ID → U_*` or 296 `H_USER_ID, H_CRYPTO_ID → H_ID`. For such an FD to fire, two rows would have to agree on that 297 left side. But only one row has `a`s in that cluster, and the `b`s of different rows are 298 all different, so no two rows ever agree on any left side. **The chase changes nothing, and 299 no row becomes all `a`.** Under FD1–FD17 alone, the ten relations are *not* guaranteed to 300 join back to `R_EDUBERZA`. This is not an accident of this model. It is exactly why 301 Bernstein's synthesis algorithm has a final step: *if no synthesised relation contains a 302 candidate key of `R`, add one that does.* None of the ten contains the ten-attribute key. 303 304 **Step 2 — add the key relation** `R_KEY(U_ID, C_ID, M_ID, H_ID, O_ID, T_ID, MT_ID, MC_ID, 305 W_ID, WI_ID)`. Its row has `a` only in the ten ID columns, written `a·` for "`a` in the ID, 306 `b` in the rest of the group": 307 308 ``` 309 U* C* M* H* O* T* MT* MC* W* WI* 310 R_KEY a· a· a· a· a· a· a· a· a· a· 311 (the ten rows of step 1 unchanged) 312 ``` 313 314 Now FD1 `U_ID → U_*` fires: row `R_KEY` and row `R_USERS` both have `a` in `U_ID`, so they 315 must agree on the rest of `U_*`, and `R_USERS` has `a` there. `R_KEY` becomes `a` in the whole 316 `U*` group. The same happens with FD4 (`C*`), FD6 (`M*`), FD8 (`H*`), FD10 (`O*`), FD11 (`T*`), 317 FD12 (`MT*`), FD13 (`MC*`), FD15 (`W*`) and FD16 (`WI*`): 318 319 ``` 320 U* C* M* H* O* T* MT* MC* W* WI* 321 R_KEY a a a a a a a a a a <- all distinguished 322 ``` 323 324 **Row `R_KEY` is all `a`, so the decomposition into the ten relations plus `R_KEY` is 325 lossless.** 326 327 **Why `R_KEY` is not kept in the final schema.** An instance of `R_KEY` would only record 328 which ID of one cluster appears together with which ID of every other cluster. As shown under 329 *Candidate keys and primary key*, the ten clusters are independent record types, and 330 `R_EDUBERZA` pairs every row of one with every row of the others. So `R_KEY` would be just the 331 cross product of the ten ID sets and would carry no information. The same independence means 332 the join dependency `⋈[R_USERS, …, R_WATCHLIST_ITEMS]` holds on `R_EDUBERZA` by construction. 333 Under that dependency the ten relations alone already reconstruct it: their natural join, with 334 no common attributes, is exactly that cross product. The chase makes this reasoning explicit. 335 FDs by themselves cannot prove the join lossless; you need either the key relation or the 336 independence of the clusters. That was hidden in the earlier "foreign key equals primary key" 337 argument, which described the equi-joins the application runs, not the natural join the 338 lossless-join property is about. 272 339 273 340 ## 3NF decomposition -
docs/README.md
ra531b45 ref1c1c7 43 43 | `P5-Normalization/` | P5 | `Normalization`, `NormalizationAIUsage` | 44 44 | `P6-AdvancedReports/` | P6 | `AdvancedReports`, `AdvancedReportsAIUsage` | 45 | `P7-AdvancedDatabaseDevelopment/` | P7 | `AdvancedDatabaseDevelopment`, `AdvancedDatabaseDevelopmentAIUsage` | 45 46 46 47 `Instructions.md` is the condensed course rubric — reference material, not a … … 65 66 | P5 | [Normalization](P5-Normalization/Normalization.md) | Finished, awaiting approval | 66 67 | P6 | [AdvancedReports](P6-AdvancedReports/AdvancedReports.md) | Finished, awaiting approval | 67 | P7 | *Advanced Database Development* | Not started|68 | P7 | [AdvancedDatabaseDevelopment](P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopment.md) | Finished, awaiting approval | 68 69 | P8 | *Advanced Application Development* | Not started | 69 70 | P9 | *Other topics (Performance, Security)* | Not started | … … 89 90 | [`relational_schema.jpg`](P2-RelationalDesign/relational_schema.jpg) | P2 | Crow's-foot diagram exported from DBeaver | 90 91 | [`../server/db/reports_demo_data.sql`](../server/db/reports_demo_data.sql) | P6 | Optional multi-quarter demo data for the two reports (not part of `-init`) | 92 | [`../server/db/advanced_db.sql`](../server/db/advanced_db.sql) | P7 | Triggers, functions, views and the background job (part of `-init`) | 93 | [`../server/db/advanced_db_tests.sql`](../server/db/advanced_db_tests.sql) | P7 | Tests for every P7 rule, rolled back at the end | 91 94 92 95 ## Use cases (P3) … … 109 112 - [UseCaseModelAIUsage](P3-UseCaseModel/UseCaseModelAIUsage.md) (P3) 110 113 - [PrototypeImplementationAIUsage](P4-Prototype/PrototypeImplementationAIUsage.md) (P4) 114 - [NormalizationAIUsage](P5-Normalization/NormalizationAIUsage.md) (P5) 115 - [AdvancedReportsAIUsage](P6-AdvancedReports/AdvancedReportsAIUsage.md) (P6) 116 - [AdvancedDatabaseDevelopmentAIUsage](P7-AdvancedDatabaseDevelopment/AdvancedDatabaseDevelopmentAIUsage.md) (P7) 111 117 112 118 ## Build & run
Note:
See TracChangeset
for help on using the changeset viewer.
