diff --git a/app/models/teller_enrollment.py b/app/models/teller_enrollment.py index c5ad4fc..76f855c 100644 --- a/app/models/teller_enrollment.py +++ b/app/models/teller_enrollment.py @@ -52,3 +52,25 @@ class TellerAccount(db.Model): def __repr__(self): return f'' + + +class TellerSyncPreview(db.Model): + """ + Temporary server-side storage for sync preview data. + Replaces cookie-based session storage which has a 4 KB limit — + large accounts (100+ transactions) exceeded that limit and caused + silent data loss on the confirm step. + One row per teller_account_id; overwritten on each new sync preview. + """ + __tablename__ = 'teller_sync_previews' + + id = db.Column(db.Integer, primary_key=True) + teller_account_id = db.Column( + db.Integer, db.ForeignKey('teller_accounts.id'), + nullable=False, unique=True, index=True, + ) + data_json = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' diff --git a/app/routes/teller.py b/app/routes/teller.py index 7ce1e8a..9004ace 100644 --- a/app/routes/teller.py +++ b/app/routes/teller.py @@ -7,7 +7,7 @@ from flask import (Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app, session) from flask_login import login_required, current_user from app.extensions import db -from app.models.teller_enrollment import TellerEnrollment, TellerAccount +from app.models.teller_enrollment import TellerEnrollment, TellerAccount, TellerSyncPreview from app.models.account import Account from app.services.teller_service import ( get_accounts, get_balance, sync_preview, import_transactions, @@ -183,13 +183,14 @@ def sync_preview_view(teller_account_id): flash(f'Sync failed: {e}', 'danger') return redirect(url_for('teller.index')) - # Store preview in session for confirm step - session['teller_preview'] = [ + # Store preview in DB (not session cookie — cookie limit is 4 KB; + # large accounts with 100+ transactions silently exceeded it). + preview_data = [ { 'teller_id': p['teller_id'], 'date': p['date'].isoformat(), 'transaction_type': p['transaction_type'], - 'amount': p['amount'], + 'amount': float(p['amount']), 'description': p['description'], 'account_id': p['account_id'], 'category_id': p['category_id'], @@ -197,6 +198,19 @@ def sync_preview_view(teller_account_id): } for p in preview ] + # Upsert: one preview row per teller account (overwrite any stale preview) + existing_sp = TellerSyncPreview.query.filter_by(teller_account_id=teller_account_id).first() + if existing_sp: + existing_sp.data_json = json.dumps(preview_data) + existing_sp.created_at = __import__('datetime').datetime.utcnow() + sp = existing_sp + else: + sp = TellerSyncPreview(teller_account_id=teller_account_id, + data_json=json.dumps(preview_data)) + db.session.add(sp) + db.session.commit() + # Only the tiny row ID goes into the session cookie + session['teller_preview_id'] = sp.id session['teller_account_id'] = teller_account_id from app.models.category import Category @@ -213,13 +227,22 @@ def sync_preview_view(teller_account_id): @login_required def sync_confirm(): """Import selected previewed transactions with optional type overrides.""" - raw = session.pop('teller_preview', []) - ta_id = session.pop('teller_account_id', None) + preview_id = session.pop('teller_preview_id', None) + ta_id = session.pop('teller_account_id', None) - if not raw or not ta_id: + if not preview_id or not ta_id: flash('No pending import. Please sync again.', 'warning') return redirect(url_for('teller.index')) + sp = db.session.get(TellerSyncPreview, preview_id) + if not sp: + flash('Preview expired or already imported. Please sync again.', 'warning') + return redirect(url_for('teller.index')) + + raw = json.loads(sp.data_json) + db.session.delete(sp) + db.session.commit() + ta = db.get_or_404(TellerAccount, ta_id) selected_ids = set(request.form.getlist('selected'))