diff --git a/app/routes/schwab.py b/app/routes/schwab.py index 27e943d..9b30969 100644 --- a/app/routes/schwab.py +++ b/app/routes/schwab.py @@ -10,7 +10,7 @@ from app.models.account import Account from app.models.schwab_connection import SchwabConnection, SchwabAccount from app.services.schwab_service import ( get_auth_url, exchange_code, _apply_token_data, - get_accounts, sync_preview, import_transactions, + get_account_number_hashes, get_accounts, sync_preview, import_transactions, ACCOUNT_TYPE_MAP, ) @@ -74,8 +74,9 @@ def callback(): db.session.add(conn) db.session.flush() - # Fetch accounts and store them + # Fetch account hash mapping first (hashValue is required for all API paths) try: + hash_map = get_account_number_hashes(conn) raw_accounts = get_accounts(conn) except Exception as e: log.error('[schwab] get_accounts failed: %s', e, exc_info=True) @@ -84,13 +85,14 @@ def callback(): return redirect(url_for('schwab.index')) for ra in raw_accounts: - sec = ra.get('securitiesAccount', {}) - acct_hash = sec.get('accountNumber', '') + sec = ra.get('securitiesAccount', {}) + acct_num = sec.get('accountNumber', '') + acct_hash = hash_map.get(acct_num, acct_num) # use hashValue, fall back to raw number if not acct_hash: continue existing = SchwabAccount.query.filter_by(account_hash=acct_hash).first() if not existing: - masked = '…' + acct_hash[-4:] if len(acct_hash) >= 4 else acct_hash + masked = '…' + acct_num[-4:] if len(acct_num) >= 4 else acct_num db.session.add(SchwabAccount( connection=conn, account_hash=acct_hash, diff --git a/app/services/schwab_service.py b/app/services/schwab_service.py index 7af7f28..4bf0fc0 100644 --- a/app/services/schwab_service.py +++ b/app/services/schwab_service.py @@ -163,6 +163,17 @@ def _raise_for_status(resp, context=''): # ── Public API helpers ──────────────────────────────────────────────────────── +def get_account_number_hashes(connection): + """ + Return {accountNumber: hashValue} mapping. + Schwab requires the hashValue (encrypted account number) in all endpoint paths. + """ + data = _authed_get(connection, '/trader/v1/accounts/accountNumbers') + mapping = {item['accountNumber']: item['hashValue'] for item in data} + log.info('[schwab] get_account_number_hashes: %d account(s)', len(mapping)) + return mapping + + def get_accounts(connection): """Return list of Schwab account dicts.""" data = _authed_get(connection, '/trader/v1/accounts', params={'fields': 'positions'})