# Live Vault Balance From `/money` Endpoint — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace `MAX(running_balance)` (which uses stale `/log` data) with the live `/money` endpoint's `vault` field for the balances shown in the right-side panel. Falls back to the SQL MAX when the API fails. **Architecture:** New `fetchLiveVaultBalance(string $user): ?int` helper in `includes/utilities.php`. Hits `https://api.torn.com/v2/user?selections=money` with the user's API key. Returns the `money.vault` integer or null on failure. In-memory static cache so multiple calls per request reuse one fetch. **Tech Stack:** PHP 8.1+, SQLite3 via PDO, cURL. No new dependencies. ## Global Constraints - PHP 8.1+ syntax only. - API authentication: `Authorization: ApiKey ` header. - Idempotency: every change must be safe to re-run. - The `running_balance` column, the "Balance After" column in `buildTable`, and the `backfillRunningBalances` helper are unchanged. This plan only changes the *displayed* balance (the right-side panel), not the per-entry historical record. - Falls back gracefully when the API fails — the page must still render. - Commits per task. Do not push. - All existing helpers outside the rewritten ones stay byte-identical. --- ### Task 1: Write the failing test for `fetchLiveVaultBalance` **Files:** - Create: `tests/live_balance_test.php` **Interfaces (the test exercises these):** - `fetchLiveVaultBalance(string $user): ?int` - On success, returns the integer `vault` amount from `/money`. - On API failure (network error, bad JSON, missing field), returns null. - Caches the result within a single PHP request — multiple calls for the same user do not re-fetch. The test does NOT need a live API call. It exercises the helper by either: - Mocking at the `executeApiCall` boundary (preferred — test swaps in a stub function), OR - Mocking the API response via a wrapper. Choose whichever is cleanest. Since `executeApiCall` is a plain PHP function (not a class method), wrapping it for testing is awkward. Instead, design the helper to accept an optional override URL, OR refactor the helper to internally call a swappable transport function. **Recommended approach:** Add a static class property or file-static variable `$liveBalanceTestHook` in `includes/utilities.php` that defaults to null. If set, the helper calls it instead of `executeApiCall`. The test sets the hook before calling the helper, then unsets it. This is a 2-line addition to the helper and makes the test trivial. Don't add a sophisticated test harness. - [ ] **Step 1: Create the test file** Create `tests/live_balance_test.php`: ```php 'test-key-not-used', 'symos' => 'test-key-not-used', ]); } require_once __DIR__ . '/../includes/exceptions.php'; require_once __DIR__ . '/../includes/utilities.php'; function fail(string $message): void { fwrite(STDERR, "FAIL: $message\n"); exit(1); } function assertSame($expected, $actual, string $label): void { if ($expected !== $actual) { $exp = var_export($expected, true); $act = var_export($actual, true); fail("$label: expected $exp, got $act"); } } // Test hook: when set, fetchLiveVaultBalance calls this closure instead of // executeApiCall. The closure receives ($url, $apiKey) and returns the // decoded response array (or throws). $GLOBALS['liveBalanceTestHook'] = null; function liveBalanceHook($url, $apiKey) { $hook = $GLOBALS['liveBalanceTestHook'] ?? null; if ($hook === null) { fail('Test hook not set — fetchLiveVaultBalance called executeApiCall'); } return $hook($url, $apiKey); } // --- Test 1: success path returns the vault amount as int --- $GLOBALS['liveBalanceTestHook'] = function ($url, $apiKey) { assertSame('zarathos', $apiKey === 'test-key-not-used' ? 'zarathos' : 'symos', 'api key routed'); // (just verify the hook got called) return ['money' => ['vault' => 160627669, 'wallet' => 4000]]; }; $balance = fetchLiveVaultBalance('zarathos'); assertSame(160627669, $balance, 'success returns vault amount'); // --- Test 2: failure path returns null --- $GLOBALS['liveBalanceTestHook'] = function () { throw new CurlErrorException('connection refused'); }; $balance = fetchLiveVaultBalance('zarathos'); assertSame(null, $balance, 'failure returns null'); // --- Test 3: missing money.vault returns null --- $GLOBALS['liveBalanceTestHook'] = function () { return ['error' => ['code' => 4]]; }; $balance = fetchLiveVaultBalance('zarathos'); assertSame(null, $balance, 'missing field returns null'); // --- Test 4: cache: second call does not invoke the hook twice --- $invocations = 0; $GLOBALS['liveBalanceTestHook'] = function () use (&$invocations) { $invocations++; return ['money' => ['vault' => 100]]; }; fetchLiveVaultBalance('symos'); fetchLiveVaultBalance('symos'); fetchLiveVaultBalance('symos'); assertSame(1, $invocations, 'cache prevents repeated fetches'); echo "OK: fetchLiveVaultBalance tests passed.\n"; ``` The test sets `$GLOBALS['liveBalanceTestHook']` and the helper must check this global and call it instead of `executeApiCall`. This is the swappable-transport mechanism described above. - [ ] **Step 2: Run the test and verify it fails** Run: `php tests/live_balance_test.php` Expected: failure with `Test hook not set` (because the helper doesn't exist yet, and even if it did, the test hook mechanism isn't there yet). - [ ] **Step 3: Commit the failing test** ```bash git add tests/live_balance_test.php git commit -m "Add failing test for fetchLiveVaultBalance" ``` --- ### Task 2: Implement `fetchLiveVaultBalance` with test hook **Files:** - Modify: `includes/utilities.php` (append new function + a tiny test-hook shim) **Interfaces:** - `fetchLiveVaultBalance(string $user): ?int` - Returns `int` on success (the `money.vault` value). - Returns `null` on any failure (no exception is raised). - Per-request in-memory cache: subsequent calls for the same user return the cached value without hitting the API. - [ ] **Step 1: Add the test-hook shim** At the very top of `includes/utilities.php`, immediately after the `require_once` lines that pull in exceptions, add (only if the helper is being unit-tested): ```php // Test hook for fetchLiveVaultBalance(). When set, this closure is called // instead of executeApiCall. Set $GLOBALS['liveBalanceTestHook'] to a // closure($url, $apiKey): array in test code. Production code leaves it null. $GLOBALS['liveBalanceTestHook'] = $GLOBALS['liveBalanceTestHook'] ?? null; ``` (If the file already requires things at the top, just place the line after the existing requires. The point is: the helper checks this global.) - [ ] **Step 2: Append the new function** Append this function to `includes/utilities.php` (after `consoleLog`): ```php /** * Fetch the live vault balance for a user from the v2 /money endpoint. * * Returns the `money.vault` value as an int, or null if the API call * fails, returns invalid JSON, or doesn't include `money.vault`. The * result is cached for the duration of the PHP request so multiple * callers (e.g., `vaultLoop` for one user, then the all-users sum) * don't re-fetch. * * @param string $user The user whose vault balance to fetch. * * @return int|null The vault amount in pennies, or null on failure. */ function fetchLiveVaultBalance($user) { static $cache = []; if (!array_key_exists($user, $cache)) { if (!array_key_exists($user, USER_KEYS)) { $cache[$user] = null; return null; } $url = 'https://api.torn.com/v2/user?selections=money'; try { $hook = $GLOBALS['liveBalanceTestHook'] ?? null; $responseData = $hook !== null ? $hook($url, USER_KEYS[$user]) : executeApiCall($url, USER_KEYS[$user]); if (!isset($responseData['money']['vault'])) { $cache[$user] = null; return null; } $cache[$user] = (int)$responseData['money']['vault']; } catch (Exception $e) { $cache[$user] = null; } } return $cache[$user]; } ``` - [ ] **Step 3: Run the test and verify it passes** Run: `php tests/live_balance_test.php` Expected: `OK: fetchLiveVaultBalance tests passed.` - [ ] **Step 4: Run the existing golden-file test to ensure no regression** Run: `php tests/process_log_entries_test.php` Expected: `OK: processLogEntries v2 golden-file test passed (100 entries).` - [ ] **Step 5: Verify the file parses** Run: `php -l includes/utilities.php` Expected: `No syntax errors detected`. - [ ] **Step 6: Commit** ```bash git add includes/utilities.php git commit -m "Add fetchLiveVaultBalance helper using v2 /money endpoint" ``` --- ### Task 3: Update `vaultLoop` to use the live balance with fallback **Files:** - Modify: `includes/utilities.php` (`vaultLoop` only) **Interfaces:** - `vaultLoop($name=null)`: returns the live vault balance (from `fetchLiveVaultBalance`) when available; falls back to `MAX(running_balance)` when the helper returns null. Behavior contract unchanged: returns int, or 0 when nothing applies. - [ ] **Step 1: Replace `vaultLoop`** In `includes/utilities.php`, replace the existing `vaultLoop` function (at the SQL-based implementation from the previous balance-fix plan) with: ```php function vaultLoop ($name=null) { $liveBalance = $name === null ? null : fetchLiveVaultBalance($name); if ($liveBalance !== null) { return $liveBalance; } // Fallback: derive from stored entries. Used when the live /money call // fails or is not configured for the user. $pdo = getDatabaseConnection(); if ($name === null) { $stmt = $pdo->query( 'SELECT COALESCE(SUM(max_balance), 0) FROM ' . '(SELECT MAX(running_balance) AS max_balance FROM vault GROUP BY user)' ); return (int)$stmt->fetchColumn(); } $stmt = $pdo->prepare('SELECT MAX(running_balance) FROM vault WHERE user = :user'); $stmt->bindValue(':user', $name); $stmt->execute(); return (int)$stmt->fetchColumn(); } ``` The all-users path (`$name === null`) skips the live helper entirely and goes straight to the SQL fallback — because the live helper is per-user; aggregating live values would require N HTTP calls, which we don't want on every page load. The all-users total displayed in the right-side panel will be the sum of per-user MAX(running_balance) fallback values — fine for the migration window and can be improved in a follow-up if it ever matters. - [ ] **Step 2: Run the existing test to ensure no regression** Run: `php tests/process_log_entries_test.php` Expected: `OK: processLogEntries v2 golden-file test passed (100 entries).` - [ ] **Step 3: Run the new test to ensure it still passes** Run: `php tests/live_balance_test.php` Expected: `OK: fetchLiveVaultBalance tests passed.` - [ ] **Step 4: Verify the file parses** Run: `php -l includes/utilities.php` Expected: `No syntax errors detected`. - [ ] **Step 5: Commit** ```bash git add includes/utilities.php git commit -m "vaultLoop: prefer live /money balance, fall back to MAX(running_balance)" ``` --- ### Task 4: End-to-end smoke check **Files:** No code changes. Verification only. - [ ] **Step 1: Run the test suite** Run both tests: ```bash cd "C:\Users\ksolo\Projects\Games\Torn\Torn Vault Tracker" php tests/process_log_entries_test.php php tests/live_balance_test.php ``` Expected: both print OK. - [ ] **Step 2: Verify the live DB state** ```bash cd "C:\Users\ksolo\Projects\Games\Torn\Torn Vault Tracker" php -r " \$db = new PDO('sqlite:data/vault.db'); foreach(\$db->query('SELECT user, COUNT(*) AS n, MAX(running_balance) AS b FROM vault GROUP BY user') as \$r) { echo \$r['user'] . ': ' . \$r['n'] . ' rows, MAX(running_balance)=\$' . number_format(\$r['b']) . PHP_EOL; } " ``` Expected: per-user row counts unchanged; `MAX(running_balance)` values may match the new live balance or not depending on API state. - [ ] **Step 3: Probe the test instance** ``` curl -s http://127.0.0.1:8001/ | grep -E 'class=\"(user|vault)\"|

|

' ``` Expected: the right-side "Zarathos balance:" and "Symos balance:" values should be the LIVE values from the API. They will match the `/money` endpoint's `vault` field for the appropriate API key. If the keys in `config.php` are swapped (a separate concern the user is aware of), the displayed values will be reversed from the user's actual balance — that is expected behavior pending the user's manual config edit. - [ ] **Step 4: Verify no v1 references remain** ```bash cd "C:\Users\ksolo\Projects\Games\Torn\Torn Vault Tracker" grep -n "firstRun\|?key=" functions.php index.php includes/utilities.php includes/exceptions.php ``` Expected: no matches. - [ ] **Step 5: Commit if any incidental fixes were needed** If Step 1, 2, 3, or 4 produced any required fixes, commit them. Otherwise, no commit. --- ## Self-Review Notes **Spec coverage:** | Requirement | Task | |---|---| | Use live `/money` endpoint for displayed balances | Task 2 (helper) + Task 3 (vaultLoop) | | Fall back gracefully when API fails | Task 2 (try/catch) + Task 3 (fallback path) | | Per-request cache to avoid N HTTP calls | Task 2 (static `$cache`) | | Test the helper without making live API calls | Task 1 (test hook) | | Existing tests still pass | Tasks 2 & 3 (re-run both tests after each) | | End-to-end smoke check | Task 4 | **Placeholder scan:** No "TODO", "TBD", "implement later". **Type consistency:** `fetchLiveVaultBalance(string $user): ?int` returns int|null. `vaultLoop` consumes `?int` and returns int. `generateBalance`/`getSpace` wrap `vaultLoop` and continue to work. **Risks addressed:** - API failure → null → fallback to MAX(running_balance) → page renders. No change in failure mode. - Hook abuse → only affects test execution. In production, `$GLOBALS['liveBalanceTestHook']` is null (set by the one-line shim) and `executeApiCall` runs. - All-users path uses SQL fallback, not live; document this in the function code so future readers understand.