From d8608c7058e471c4bec941750cec4925167670a2 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 1 Jun 2026 17:42:25 -0400 Subject: [PATCH] 06/01 Optimize account balances --- app/routes/teller.py | 48 ++++++++++++++++++++- app/templates/teller/index.html | 74 ++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/app/routes/teller.py b/app/routes/teller.py index 12175a1..fb93ca4 100644 --- a/app/routes/teller.py +++ b/app/routes/teller.py @@ -279,14 +279,58 @@ def refresh_balance(teller_account_id): try: bal_data = get_balance(ta.enrollment.access_token, ta.teller_account_id) available = float(bal_data.get('available') or bal_data.get('ledger') or 0) + ledger = float(bal_data.get('ledger') or bal_data.get('available') or 0) ta.pfm_account.balance = available db.session.commit() - return jsonify({'balance': available, 'status': 'ok'}) + log.info('[teller] balance refreshed for %s: available=%.2f ledger=%.2f', + ta.account_name, available, ledger) + return jsonify({ + 'status': 'ok', + 'available': available, + 'ledger': ledger, + 'account': ta.account_name, + }) except Exception as e: - log.error('[teller] balance refresh failed for teller_account_id=%s: %s', teller_account_id, e, exc_info=True) + log.error('[teller] balance refresh failed for teller_account_id=%s: %s', + teller_account_id, e, exc_info=True) return jsonify({'error': str(e)}), 502 +@teller_bp.route('/balance/all', methods=['POST']) +@login_required +def refresh_all_balances(): + """Refresh live balances for every mapped Teller account across all enrollments.""" + enrollments = TellerEnrollment.query.filter_by(is_active=True).all() + refreshed, failed = [], [] + + for enrollment in enrollments: + for ta in enrollment.accounts.filter_by(is_active=True).all(): + if not ta.pfm_account_id: + continue + try: + bal_data = get_balance(enrollment.access_token, ta.teller_account_id) + available = float(bal_data.get('available') or bal_data.get('ledger') or 0) + ta.pfm_account.balance = available + refreshed.append(ta.account_name) + log.info('[teller] balance refreshed: %s = %.2f', ta.account_name, available) + except Exception as exc: + failed.append(ta.account_name) + log.error('[teller] balance refresh failed for %s: %s', + ta.account_name, exc, exc_info=True) + + if refreshed or failed: + db.session.commit() + + if refreshed: + flash(f'Balances updated for: {", ".join(refreshed)}.', 'success') + if failed: + flash(f'Failed to refresh: {", ".join(failed)}. Check System Logs for details.', 'danger') + if not refreshed and not failed: + flash('No mapped accounts found to refresh.', 'warning') + + return redirect(url_for('teller.index')) + + # ── Disconnect ──────────────────────────────────────────────────────────────── @teller_bp.route('/disconnect/', methods=['POST']) diff --git a/app/templates/teller/index.html b/app/templates/teller/index.html index 11cb2c5..7fd9f16 100644 --- a/app/templates/teller/index.html +++ b/app/templates/teller/index.html @@ -3,6 +3,15 @@ {% block page_title %}Bank Connections{% endblock %} {% block topbar_actions %} +{% if enrollments %} +
+ + +
+{% endif %} - Sync @@ -182,7 +205,8 @@ initTellerConnect(document.getElementById('tellerConnectBtn2')); function refreshBalance(taId, btn) { btn.disabled = true; - btn.innerHTML = ''; + btn.innerHTML = ''; + fetch('/teller/balance/' + taId, { method: 'POST', headers: { 'X-CSRFToken': CSRF }, @@ -191,20 +215,38 @@ function refreshBalance(taId, btn) { .then(data => { btn.disabled = false; btn.innerHTML = ''; - if (data.balance != null) { + + if (data.available != null) { const sym = '{{ current_user.currency_symbol }}'; - btn.title = 'Balance: ' + sym + parseFloat(data.balance).toLocaleString(undefined, {minimumFractionDigits:2}); + const fmt = v => sym + parseFloat(v).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2}); + + // Update inline balance display + const balEl = document.getElementById('bal-' + taId); + if (balEl) { + balEl.textContent = fmt(data.available); + balEl.style.color = '#10b981'; + setTimeout(() => balEl.style.color = '', 2500); + } + + // Show available vs ledger in button tooltip + const ledgerNote = (data.ledger !== data.available) + ? ` · Ledger: ${fmt(data.ledger)}` + : ''; + btn.title = `Available: ${fmt(data.available)}${ledgerNote} — updated just now`; btn.style.color = '#10b981'; - setTimeout(() => btn.style.color = '', 3000); + setTimeout(() => { btn.style.color = ''; btn.title = 'Pull live balance from bank'; }, 3000); } else { - btn.title = data.error || 'Failed'; + btn.title = data.error || 'Refresh failed'; btn.style.color = '#ef4444'; - setTimeout(() => btn.style.color = '', 3000); + setTimeout(() => { btn.style.color = ''; btn.title = 'Pull live balance from bank'; }, 4000); } }) .catch(() => { btn.disabled = false; btn.innerHTML = ''; + btn.title = 'Network error — try again'; + btn.style.color = '#ef4444'; + setTimeout(() => { btn.style.color = ''; btn.title = 'Pull live balance from bank'; }, 4000); }); }