from datetime import datetime from app.extensions import db from app.utils.crypto import EncryptedText class PlaidItem(db.Model): """One Plaid Item = one bank connection (may contain multiple accounts).""" __tablename__ = 'plaid_items' id = db.Column(db.Integer, primary_key=True) item_id = db.Column(db.String(100), unique=True, nullable=False, index=True) access_token = db.Column(EncryptedText, nullable=False) institution_id = db.Column(db.String(50), nullable=True) institution_name = db.Column(db.String(100), nullable=True) cursor = db.Column(db.String(500), nullable=True) # /transactions/sync cursor is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_synced_at = db.Column(db.DateTime, nullable=True) accounts = db.relationship('PlaidAccount', back_populates='item', cascade='all, delete-orphan', lazy='dynamic') def __repr__(self): return f'' class PlaidAccount(db.Model): """Maps one Plaid account inside an item to a PFM account.""" __tablename__ = 'plaid_accounts' id = db.Column(db.Integer, primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey('plaid_items.id'), nullable=False) plaid_account_id = db.Column(db.String(100), unique=True, nullable=False, index=True) pfm_account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=True) account_name = db.Column(db.String(100), nullable=True) account_type = db.Column(db.String(50), nullable=True) # depository / credit / investment account_subtype = db.Column(db.String(50), nullable=True) # checking / savings / credit card mask = db.Column(db.String(10), nullable=True) # last 4 digits last_sync_date = db.Column(db.Date, nullable=True) # Credit card billing info (populated from /liabilities/get) cc_due_date = db.Column(db.Date, nullable=True) cc_minimum_payment = db.Column(db.Numeric(12, 2), nullable=True) cc_last_statement_balance = db.Column(db.Numeric(12, 2), nullable=True) cc_is_overdue = db.Column(db.Boolean, default=False) cc_updated_at = db.Column(db.DateTime, nullable=True) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) item = db.relationship('PlaidItem', back_populates='accounts') pfm_account = db.relationship('Account') @property def display_name(self): suffix = f' ••••{self.mask}' if self.mask else '' return f'{self.account_name}{suffix}' def __repr__(self): return f'' class PlaidSyncPreview(db.Model): """Temporary server-side storage for Plaid sync preview data (per item).""" __tablename__ = 'plaid_sync_previews' id = db.Column(db.Integer, primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey('plaid_items.id'), nullable=False, unique=True, index=True) data_json = db.Column(db.Text, nullable=False) # JSON array of parsed transactions next_cursor = db.Column(db.String(500), nullable=True) # advance item cursor on confirm created_at = db.Column(db.DateTime, default=datetime.utcnow) def __repr__(self): return f''